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}