Skip to main content

oxiblas_core/
simd.rs

1//! SIMD abstraction layer for OxiBLAS.
2//!
3//! This module provides a unified, general-purpose interface over
4//! architecture-specific SIMD intrinsics from `core::arch`. It supports:
5//! - x86_64: AVX2 (256-bit), AVX512F (512-bit), SSE4.2 (128-bit)
6//! - AArch64: NEON (128-bit), 256-bit emulated
7//! - WASM32: SIMD128 (128-bit), 256-bit emulated
8//! - Scalar fallback for unsupported platforms
9//!
10//! The design uses runtime feature detection to dispatch to the best
11//! available implementation.
12//!
13//! # Integration status (read this before assuming it is on the hot path)
14//!
15//! This is a *general-purpose* SIMD toolkit that is available for downstream
16//! use, but it is **not** the code that powers OxiBLAS's tuned compute kernels.
17//! To avoid advertising an integration that does not exist:
18//!
19//! - The BLAS/LAPACK compute kernels in `oxiblas-blas` each perform their own
20//!   `is_x86_feature_detected!`-guarded dispatch and carry their own
21//!   hand-written intrinsics. As of this writing that includes
22//!   `level1::{dot, axpy, nrm2}`, `level2::gemv`, and
23//!   `level3::{gemm, gemm_kernel, gemm_packing, gemm_small, gemm_kernel_sse42}`.
24//!   None of them call the [`SimdRegister`]/[`SimdScalar`] register API below.
25//! - What *is* consumed outside this module's own tests is the lightweight
26//!   capability layer: [`SimdLevel`] and [`detect_simd_level`] /
27//!   [`detect_simd_level_raw`], which `crate::tuning` uses for cache/block-size
28//!   heuristics and which the crate re-exports.
29//! - The register-level API ([`SimdRegister`], [`SimdScalar`], the concrete
30//!   register types, and the [`complex`], [`dispatch`], [`multiver`], and
31//!   [`scalar`] submodules) is currently exercised only by this crate's unit
32//!   tests and the `simd` benchmark. It is correct and covered, but no kernel
33//!   is wired to it yet.
34//!
35//! A downstream Level-1 loop would adopt this layer by pairing [`SimdChunks`]
36//! (for head/body/tail splitting) with a [`SimdRegister`] type, as the
37//! `test_abstraction_level1_dot_matches_scalar` regression test demonstrates.
38//!
39//! # Safety: the target-feature contract of the wide registers
40//!
41//! The 128-bit SSE register methods are sound on x86_64 because SSE2 is part of
42//! the x86_64 baseline (the FMA path is selected at compile time via
43//! `#[cfg(target_feature = "fma")]`). The **256-bit (AVX2) and 512-bit
44//! (AVX-512) register methods are declared as safe `fn`s but emit AVX2/AVX-512
45//! instructions without an internal runtime feature guard**. Executing those
46//! instructions on a CPU that lacks the feature is undefined behavior, so a
47//! caller MUST confirm the feature is present (e.g. via
48//! `is_x86_feature_detected!("avx2")`) before constructing or operating on
49//! those register types. This unguarded contract is the reason the tuned
50//! kernels — and this module's own tests — gate every wide-register use behind
51//! runtime detection rather than calling the abstraction blindly.
52//!
53//! # Complex SIMD
54//!
55//! The `complex` submodule provides SIMD types for complex numbers in
56//! interleaved format `[re0, im0, re1, im1, ...]`.
57
58#[cfg(target_arch = "x86_64")]
59pub mod x86_64;
60
61#[cfg(target_arch = "aarch64")]
62pub mod aarch64;
63
64#[cfg(target_arch = "wasm32")]
65pub mod wasm32;
66
67pub mod complex;
68pub mod dispatch;
69pub mod multiver;
70pub mod scalar;
71
72use crate::scalar::{Field, Real, Scalar};
73
74/// SIMD capability level detected at runtime.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
76pub enum SimdLevel {
77    /// No SIMD, scalar operations only
78    Scalar,
79    /// 128-bit SIMD (SSE2 on x86, NEON on ARM)
80    Simd128,
81    /// 256-bit SIMD (AVX2 on x86)
82    Simd256,
83    /// 512-bit SIMD (AVX512F on x86)
84    Simd512,
85}
86
87impl SimdLevel {
88    /// Returns the number of lanes for a given scalar type.
89    #[inline]
90    pub const fn lanes<T: Scalar>(self) -> usize {
91        match self {
92            SimdLevel::Scalar => 1,
93            SimdLevel::Simd128 => 16 / core::mem::size_of::<T>(),
94            SimdLevel::Simd256 => 32 / core::mem::size_of::<T>(),
95            SimdLevel::Simd512 => 64 / core::mem::size_of::<T>(),
96        }
97    }
98
99    /// Returns the register width in bytes.
100    #[inline]
101    pub const fn width_bytes(self) -> usize {
102        match self {
103            SimdLevel::Scalar => 8, // Treat as 64-bit for alignment
104            SimdLevel::Simd128 => 16,
105            SimdLevel::Simd256 => 32,
106            SimdLevel::Simd512 => 64,
107        }
108    }
109}
110
111/// Detects the best available SIMD level at runtime.
112///
113/// This function respects the following feature flags:
114/// - `force-scalar`: Always returns `SimdLevel::Scalar` (useful for debugging)
115/// - `max-simd-128`: Limits maximum to `SimdLevel::Simd128`
116/// - `max-simd-256`: Limits maximum to `SimdLevel::Simd256`
117#[inline]
118pub fn detect_simd_level() -> SimdLevel {
119    // Feature flag: force scalar operations (useful for debugging)
120    #[cfg(feature = "force-scalar")]
121    {
122        SimdLevel::Scalar
123    }
124
125    #[cfg(not(feature = "force-scalar"))]
126    {
127        let detected = detect_simd_level_raw();
128
129        // Apply maximum SIMD level limits from feature flags
130        #[cfg(feature = "max-simd-128")]
131        {
132            return if detected > SimdLevel::Simd128 {
133                SimdLevel::Simd128
134            } else {
135                detected
136            };
137        }
138
139        #[cfg(feature = "max-simd-256")]
140        #[cfg(not(feature = "max-simd-128"))]
141        {
142            return if detected > SimdLevel::Simd256 {
143                SimdLevel::Simd256
144            } else {
145                detected
146            };
147        }
148
149        #[cfg(not(any(feature = "max-simd-128", feature = "max-simd-256")))]
150        {
151            detected
152        }
153    }
154}
155
156/// Raw SIMD level detection without feature flag limits.
157///
158/// This is the internal detection function that returns the actual
159/// hardware SIMD capability.
160#[inline]
161pub fn detect_simd_level_raw() -> SimdLevel {
162    // On x86_64 with std, use runtime feature detection
163    #[cfg(all(target_arch = "x86_64", feature = "std"))]
164    {
165        if is_x86_feature_detected!("avx512f") {
166            SimdLevel::Simd512
167        } else if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
168            SimdLevel::Simd256
169        } else if is_x86_feature_detected!("sse2") {
170            SimdLevel::Simd128
171        } else {
172            SimdLevel::Scalar
173        }
174    }
175
176    // On x86_64 without std, use compile-time target features only
177    #[cfg(all(target_arch = "x86_64", not(feature = "std")))]
178    {
179        #[cfg(target_feature = "avx512f")]
180        {
181            SimdLevel::Simd512
182        }
183        #[cfg(all(
184            target_feature = "avx2",
185            target_feature = "fma",
186            not(target_feature = "avx512f")
187        ))]
188        {
189            SimdLevel::Simd256
190        }
191        #[cfg(all(
192            target_feature = "sse2",
193            not(target_feature = "avx2"),
194            not(target_feature = "avx512f")
195        ))]
196        {
197            SimdLevel::Simd128
198        }
199        #[cfg(not(any(
200            target_feature = "sse2",
201            target_feature = "avx2",
202            target_feature = "avx512f"
203        )))]
204        {
205            SimdLevel::Scalar
206        }
207    }
208
209    #[cfg(target_arch = "aarch64")]
210    {
211        // NEON is always available on AArch64
212        SimdLevel::Simd128
213    }
214
215    #[cfg(target_arch = "wasm32")]
216    {
217        // WASM SIMD128 when simd128 feature is enabled
218        #[cfg(target_feature = "simd128")]
219        {
220            SimdLevel::Simd128
221        }
222        #[cfg(not(target_feature = "simd128"))]
223        {
224            SimdLevel::Scalar
225        }
226    }
227
228    #[cfg(not(any(
229        target_arch = "x86_64",
230        target_arch = "aarch64",
231        target_arch = "wasm32"
232    )))]
233    {
234        SimdLevel::Scalar
235    }
236}
237
238/// Trait for SIMD-capable scalar types.
239///
240/// This trait provides the interface for types that can be vectorized
241/// using SIMD operations.
242pub trait SimdScalar: Field {
243    /// The 256-bit SIMD register type for this scalar (e.g., AVX2 on x86-64).
244    type Simd256: SimdRegister<Scalar = Self>;
245    /// The 512-bit SIMD register type for this scalar (e.g., AVX-512 on x86-64).
246    type Simd512: SimdRegister<Scalar = Self>;
247
248    /// Number of elements that fit in a 256-bit register.
249    const LANES_256: usize = 32 / core::mem::size_of::<Self>();
250
251    /// Number of elements that fit in a 512-bit register.
252    const LANES_512: usize = 64 / core::mem::size_of::<Self>();
253}
254
255/// Trait for SIMD register types.
256///
257/// This provides a unified interface for SIMD operations across
258/// different architectures and vector widths.
259pub trait SimdRegister: Copy + Clone + Send + Sync {
260    /// The scalar type this register holds.
261    type Scalar: SimdScalar;
262
263    /// Number of lanes in this register.
264    const LANES: usize;
265
266    /// Creates a register with all lanes set to zero.
267    fn zero() -> Self;
268
269    /// Creates a register with all lanes set to the same value.
270    fn splat(value: Self::Scalar) -> Self;
271
272    /// Loads from an aligned pointer.
273    ///
274    /// # Safety
275    /// The pointer must be aligned to the register width and point to
276    /// at least LANES valid elements.
277    unsafe fn load_aligned(ptr: *const Self::Scalar) -> Self;
278
279    /// Loads from an unaligned pointer.
280    ///
281    /// # Safety
282    /// The pointer must point to at least LANES valid elements.
283    unsafe fn load_unaligned(ptr: *const Self::Scalar) -> Self;
284
285    /// Stores to an aligned pointer.
286    ///
287    /// # Safety
288    /// The pointer must be aligned to the register width and point to
289    /// at least LANES valid writable elements.
290    unsafe fn store_aligned(self, ptr: *mut Self::Scalar);
291
292    /// Stores to an unaligned pointer.
293    ///
294    /// # Safety
295    /// The pointer must point to at least LANES valid writable elements.
296    unsafe fn store_unaligned(self, ptr: *mut Self::Scalar);
297
298    /// Element-wise addition.
299    fn add(self, other: Self) -> Self;
300
301    /// Element-wise subtraction.
302    fn sub(self, other: Self) -> Self;
303
304    /// Element-wise multiplication.
305    fn mul(self, other: Self) -> Self;
306
307    /// Element-wise division.
308    fn div(self, other: Self) -> Self;
309
310    /// Fused multiply-add: self * a + b
311    fn mul_add(self, a: Self, b: Self) -> Self;
312
313    /// Fused multiply-subtract: self * a - b
314    fn mul_sub(self, a: Self, b: Self) -> Self;
315
316    /// Fused negative multiply-add: -(self * a) + b = b - self * a
317    fn neg_mul_add(self, a: Self, b: Self) -> Self;
318
319    /// Horizontal sum of all lanes.
320    fn reduce_sum(self) -> Self::Scalar;
321
322    /// Horizontal maximum of all lanes (for real types).
323    fn reduce_max(self) -> Self::Scalar
324    where
325        Self::Scalar: Real;
326
327    /// Horizontal minimum of all lanes (for real types).
328    fn reduce_min(self) -> Self::Scalar
329    where
330        Self::Scalar: Real;
331
332    /// Extracts a single lane.
333    fn extract(self, index: usize) -> Self::Scalar;
334
335    /// Inserts a value into a single lane.
336    fn insert(self, index: usize, value: Self::Scalar) -> Self;
337}
338
339/// Extension trait for masked SIMD operations.
340pub trait SimdMask: SimdRegister {
341    /// The mask type for this register.
342    type Mask: Copy + Clone;
343
344    /// Creates a mask from a boolean array.
345    fn mask_from_bools(bools: &[bool]) -> Self::Mask;
346
347    /// Masked load: only loads elements where mask is true.
348    ///
349    /// # Safety
350    /// For lanes where mask is true, the corresponding pointer element must be valid.
351    unsafe fn load_masked(ptr: *const Self::Scalar, mask: Self::Mask, default: Self) -> Self;
352
353    /// Masked store: only stores elements where mask is true.
354    ///
355    /// # Safety
356    /// For lanes where mask is true, the corresponding pointer element must be valid and writable.
357    unsafe fn store_masked(self, ptr: *mut Self::Scalar, mask: Self::Mask);
358
359    /// Blends two registers based on mask: if mask\[i\] then a\[i\] else b\[i\].
360    fn blend(mask: Self::Mask, a: Self, b: Self) -> Self;
361}
362
363/// Helper struct for iterating over SIMD chunks with proper head/body/tail handling.
364#[derive(Debug, Clone, Copy)]
365pub struct SimdChunks {
366    /// Total number of elements.
367    pub len: usize,
368    /// Number of lanes per SIMD register.
369    pub lanes: usize,
370    /// Index where head (unaligned prefix) ends.
371    pub head_end: usize,
372    /// Index where body (aligned middle) ends.
373    pub body_end: usize,
374}
375
376impl SimdChunks {
377    /// Creates a new chunk iterator for the given length and alignment.
378    #[inline]
379    pub fn new<T: Scalar>(ptr: *const T, len: usize, level: SimdLevel) -> Self {
380        let lanes = level.lanes::<T>();
381        let align = level.width_bytes();
382
383        if lanes <= 1 || len < lanes * 2 {
384            // Not worth SIMD, treat everything as head
385            return SimdChunks {
386                len,
387                lanes,
388                head_end: len,
389                body_end: len,
390            };
391        }
392
393        let addr = ptr as usize;
394        let misalign = addr % align;
395
396        let head_end = if misalign == 0 {
397            0
398        } else {
399            let elements_to_align = (align - misalign) / core::mem::size_of::<T>();
400            elements_to_align.min(len)
401        };
402
403        let remaining = len - head_end;
404        let full_vectors = remaining / lanes;
405        let body_end = head_end + full_vectors * lanes;
406
407        SimdChunks {
408            len,
409            lanes,
410            head_end,
411            body_end,
412        }
413    }
414
415    /// Returns the number of head elements (before aligned body).
416    #[inline]
417    pub fn head_len(&self) -> usize {
418        self.head_end
419    }
420
421    /// Returns the number of body elements (aligned middle).
422    #[inline]
423    pub fn body_len(&self) -> usize {
424        self.body_end - self.head_end
425    }
426
427    /// Returns the number of tail elements (after aligned body).
428    #[inline]
429    pub fn tail_len(&self) -> usize {
430        self.len - self.body_end
431    }
432
433    /// Returns the number of full SIMD vectors in the body.
434    #[inline]
435    pub fn body_vectors(&self) -> usize {
436        self.body_len() / self.lanes
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    #[cfg(not(feature = "std"))]
443    use alloc::vec;
444    #[cfg(not(feature = "std"))]
445    use alloc::vec::Vec;
446
447    use super::*;
448
449    #[test]
450    fn test_detect_simd_level() {
451        let level = detect_simd_level();
452        #[cfg(feature = "std")]
453        println!("Detected SIMD level: {:?}", level);
454
455        // When force-scalar is enabled, should always be Scalar
456        #[cfg(feature = "force-scalar")]
457        {
458            assert_eq!(level, SimdLevel::Scalar);
459            // But raw detection should still show hardware capability
460            let raw = detect_simd_level_raw();
461            println!("Raw hardware SIMD level: {:?}", raw);
462        }
463
464        // Without force-scalar, should detect hardware SIMD
465        #[cfg(not(feature = "force-scalar"))]
466        {
467            #[cfg(target_arch = "x86_64")]
468            assert!(level >= SimdLevel::Simd128);
469
470            #[cfg(target_arch = "aarch64")]
471            assert_eq!(level, SimdLevel::Simd128);
472        }
473    }
474
475    #[test]
476    fn test_simd_level_lanes() {
477        assert_eq!(SimdLevel::Simd256.lanes::<f64>(), 4);
478        assert_eq!(SimdLevel::Simd256.lanes::<f32>(), 8);
479        assert_eq!(SimdLevel::Simd512.lanes::<f64>(), 8);
480        assert_eq!(SimdLevel::Simd512.lanes::<f32>(), 16);
481    }
482
483    #[test]
484    fn test_simd_chunks() {
485        // Create a pointer with known alignment
486        let data: Vec<f64> = vec![0.0; 100];
487        let ptr = data.as_ptr();
488
489        let chunks = SimdChunks::new(ptr, 100, SimdLevel::Simd256);
490        #[cfg(feature = "std")]
491        println!(
492            "Chunks: head_end={}, body_end={}",
493            chunks.head_end, chunks.body_end
494        );
495
496        // Verify that head + body + tail = len
497        assert_eq!(
498            chunks.head_len() + chunks.body_len() + chunks.tail_len(),
499            100
500        );
501    }
502
503    // =============================================================================
504    // Comprehensive SIMD correctness tests
505    // =============================================================================
506
507    /// Test scalar fallback FMA accuracy.
508    #[test]
509    fn test_scalar_fma_accuracy() {
510        use crate::simd::scalar::ScalarF64;
511
512        let a = ScalarF64::splat(1.0 + 1e-15);
513        let b = ScalarF64::splat(1.0 + 1e-15);
514        let c = ScalarF64::splat(-(1.0 + 2e-15));
515
516        // FMA should preserve more precision than separate mul+add
517        let fma_result = a.mul_add(b, c);
518        let mul_add_result = a.mul(b).add(c);
519
520        // Both should be very small but may differ slightly
521        assert!(fma_result.0.abs() < 1e-14);
522        assert!(mul_add_result.0.abs() < 1e-14);
523    }
524
525    /// Test load/store roundtrip.
526    #[test]
527    fn test_load_store_roundtrip() {
528        use crate::simd::scalar::ScalarF64;
529
530        let values = [42.0f64, 1.5, -3.5, 1000.0];
531
532        for &val in &values {
533            let v = ScalarF64::splat(val);
534            assert_eq!(v.reduce_sum(), val);
535            assert_eq!(v.extract(0), val);
536        }
537    }
538
539    /// Test arithmetic identities.
540    #[test]
541    fn test_arithmetic_identities() {
542        use crate::simd::scalar::{ScalarF32, ScalarF64};
543
544        // Test with f64
545        let a = ScalarF64::splat(5.0);
546        let zero = ScalarF64::zero();
547        let one = ScalarF64::splat(1.0);
548
549        // a + 0 = a
550        assert_eq!(a.add(zero).0, 5.0);
551        // a - 0 = a
552        assert_eq!(a.sub(zero).0, 5.0);
553        // a * 1 = a
554        assert_eq!(a.mul(one).0, 5.0);
555        // a / 1 = a
556        assert_eq!(a.div(one).0, 5.0);
557        // a * 0 = 0
558        assert_eq!(a.mul(zero).0, 0.0);
559
560        // Test with f32
561        let a32 = ScalarF32::splat(5.0);
562        let zero32 = ScalarF32::zero();
563        let one32 = ScalarF32::splat(1.0);
564
565        assert_eq!(a32.add(zero32).0, 5.0);
566        assert_eq!(a32.mul(one32).0, 5.0);
567    }
568
569    /// Test reduction operations.
570    #[test]
571    fn test_reductions() {
572        use crate::simd::scalar::{ScalarF32, ScalarF64};
573
574        // For scalar types, all reductions return the same value
575        let a = ScalarF64::splat(42.0);
576        assert_eq!(a.reduce_sum(), 42.0);
577        assert_eq!(a.reduce_max(), 42.0);
578        assert_eq!(a.reduce_min(), 42.0);
579
580        let b = ScalarF32::splat(-3.5);
581        assert_eq!(b.reduce_sum(), -3.5);
582        assert_eq!(b.reduce_max(), -3.5);
583        assert_eq!(b.reduce_min(), -3.5);
584    }
585
586    /// Test negative value handling.
587    #[test]
588    fn test_negative_values() {
589        use crate::simd::scalar::ScalarF64;
590
591        let neg = ScalarF64::splat(-5.0);
592        let pos = ScalarF64::splat(3.0);
593
594        // -5 + 3 = -2
595        assert_eq!(neg.add(pos).0, -2.0);
596        // -5 * 3 = -15
597        assert_eq!(neg.mul(pos).0, -15.0);
598        // -5 - 3 = -8
599        assert_eq!(neg.sub(pos).0, -8.0);
600    }
601
602    /// Test FMA variants.
603    #[test]
604    fn test_fma_variants() {
605        use crate::simd::scalar::ScalarF64;
606
607        let a = ScalarF64::splat(2.0);
608        let b = ScalarF64::splat(3.0);
609        let c = ScalarF64::splat(4.0);
610
611        // mul_add: a * b + c = 2 * 3 + 4 = 10
612        assert_eq!(a.mul_add(b, c).0, 10.0);
613
614        // mul_sub: a * b - c = 2 * 3 - 4 = 2
615        assert_eq!(a.mul_sub(b, c).0, 2.0);
616
617        // neg_mul_add: -(a * b) + c = -6 + 4 = -2
618        assert_eq!(a.neg_mul_add(b, c).0, -2.0);
619    }
620
621    /// Test insert/extract operations.
622    #[test]
623    fn test_insert_extract() {
624        use crate::simd::scalar::ScalarF64;
625
626        let a = ScalarF64::splat(1.0);
627        let b = a.insert(0, 42.0);
628        assert_eq!(b.extract(0), 42.0);
629    }
630
631    /// Platform-specific tests for native SIMD.
632    #[cfg(target_arch = "aarch64")]
633    #[test]
634    fn test_aarch64_simd_correctness() {
635        use crate::simd::aarch64::{F32x4, F64x2, F64x4};
636
637        // Test F64x2
638        let a = F64x2::splat(2.0);
639        let b = F64x2::splat(3.0);
640
641        let sum = a.add(b);
642        assert_eq!(sum.extract(0), 5.0);
643        assert_eq!(sum.extract(1), 5.0);
644
645        let fma = a.mul_add(b, F64x2::splat(1.0));
646        assert_eq!(fma.extract(0), 7.0); // 2*3 + 1
647
648        // Test F64x4 (emulated)
649        let c = F64x4::splat(2.0);
650        let d = F64x4::splat(3.0);
651
652        assert_eq!(c.add(d).reduce_sum(), 20.0); // 4 * 5.0
653
654        // Test F32x4
655        let e = F32x4::splat(2.0);
656        let f = F32x4::splat(3.0);
657
658        assert_eq!(e.add(f).reduce_sum(), 20.0); // 4 * 5.0
659    }
660
661    /// The SIMD body loop relies on [`SimdChunks`] to hand it a pointer that is
662    /// aligned to the register width so a subsequent `load_aligned` is sound.
663    /// This locks in that contract across every possible starting misalignment:
664    /// the head must land the body on a width-aligned address, and
665    /// head + body + tail must partition the length with a whole number of
666    /// vectors in the body. A regression in the chunk arithmetic (e.g. the
667    /// `(align - misalign) / size_of` head computation) would fault a real
668    /// `load_aligned`; asserting it here turns that latent segfault into a clean
669    /// test failure. Nothing else currently drives `SimdChunks` past its length
670    /// bookkeeping.
671    #[test]
672    fn test_simd_chunks_alignment_contract() {
673        let level = SimdLevel::Simd256;
674        let lanes = level.lanes::<f64>();
675        let align = level.width_bytes();
676        // Vec<f64> is at least 8-byte aligned, so offsetting the base by 0..8
677        // elements sweeps every residue class modulo the 32-byte register width.
678        let backing: Vec<f64> = vec![0.0; 256];
679
680        for start in 0..8usize {
681            let ptr = unsafe { backing.as_ptr().add(start) };
682            let len = 200;
683            let chunks = SimdChunks::new(ptr, len, level);
684
685            assert_eq!(
686                chunks.head_len() + chunks.body_len() + chunks.tail_len(),
687                len,
688                "partition must be exact for start offset {start}"
689            );
690            assert_eq!(
691                chunks.body_len() % lanes,
692                0,
693                "body must be a whole number of vectors for start offset {start}"
694            );
695
696            let body_ptr = unsafe { ptr.add(chunks.head_len()) };
697            assert_eq!(
698                (body_ptr as usize) % align,
699                0,
700                "body pointer not {align}-byte aligned for start offset {start}"
701            );
702        }
703    }
704
705    /// End-to-end proof that a Level-1 dot product built on the abstraction
706    /// ([`SimdChunks`]-style splitting plus a native [`SimdRegister`]) reproduces
707    /// the scalar result, including special-value propagation. This is exactly
708    /// the property any future BLAS wiring of this layer would depend on, and no
709    /// other test exercises the chunked load / FMA-accumulate / reduce path
710    /// together on a real wide register.
711    ///
712    /// # Safety
713    /// The 256-bit register methods emit AVX2/FMA instructions with no internal
714    /// runtime guard (see the module-level docs), so the whole body is gated
715    /// behind `is_x86_feature_detected!`; on hardware without AVX2+FMA the check
716    /// is skipped rather than risking undefined behavior.
717    #[cfg(all(target_arch = "x86_64", feature = "std"))]
718    #[test]
719    fn test_abstraction_level1_dot_matches_scalar() {
720        use crate::simd::SimdScalar;
721
722        if !(is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma")) {
723            // No sound way to run the wide-register path; the scalar-fallback
724            // arithmetic is already covered by the other tests in this module.
725            return;
726        }
727
728        // Dot product through the abstraction: FMA-accumulate full vectors, then
729        // finish the ragged tail with scalars — the canonical Level-1 shape.
730        fn simd_dot(a: &[f64], b: &[f64]) -> f64 {
731            assert_eq!(a.len(), b.len());
732            type Reg = <f64 as SimdScalar>::Simd256;
733            let lanes = Reg::LANES;
734            let len = a.len();
735            let full = len / lanes * lanes;
736
737            let mut acc = Reg::zero();
738            let mut i = 0;
739            while i < full {
740                // SAFETY: `i + lanes <= full <= len`, so both loads read `lanes`
741                // in-bounds elements; the caller confirmed AVX2+FMA is present.
742                let va = unsafe { Reg::load_unaligned(a.as_ptr().add(i)) };
743                let vb = unsafe { Reg::load_unaligned(b.as_ptr().add(i)) };
744                acc = va.mul_add(vb, acc);
745                i += lanes;
746            }
747            let mut sum = acc.reduce_sum();
748            while i < len {
749                sum += a[i] * b[i];
750                i += 1;
751            }
752            sum
753        }
754
755        // Length deliberately not a multiple of 4 to force the scalar tail.
756        let len = 103usize;
757        let a: Vec<f64> = (0..len).map(|k| (k as f64) * 0.5 - 7.0).collect();
758        let b: Vec<f64> = (0..len).map(|k| 1.0 / (k as f64 + 1.0)).collect();
759
760        let reference: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
761        let got = simd_dot(&a, &b);
762
763        // FMA accumulation can differ from separate mul+add in the final ULPs,
764        // so compare with a tight relative tolerance rather than bit-for-bit.
765        let tol = 1e-12 * reference.abs().max(1.0);
766        assert!(
767            (got - reference).abs() <= tol,
768            "simd dot {got} vs scalar {reference} (tol {tol})"
769        );
770
771        // A NaN in the vectorized body must propagate through load/FMA/reduce.
772        let mut a_nan = a.clone();
773        a_nan[50] = f64::NAN;
774        assert!(simd_dot(&a_nan, &b).is_nan(), "NaN did not propagate");
775
776        // +Inf against a positive weight must yield an infinite result (no
777        // silent saturation to a finite value).
778        let mut a_inf = a.clone();
779        a_inf[10] = f64::INFINITY;
780        assert!(simd_dot(&a_inf, &b).is_infinite(), "Inf did not propagate");
781    }
782}