Skip to main content

bixverse_core/utils/
simd.rs

1//! Targeted SIMD implementations to accelerate specific hot loops in bixverse
2//!
3//! The 128-bit reduction arms go through `wide`, which lowers to SSE2 on
4//! x86_64 and NEON on aarch64. Both are baseline for their architecture, so
5//! both are real in a stock build, and `wide` keeps them to one code path.
6//!
7//! The 256- and 512-bit arms are hand-written `std::arch::x86_64` intrinsics
8//! behind `#[target_feature]`. `wide`'s wider types cannot be used here:
9//! `f32x8` is only backed by `__m256` when the *whole crate* is compiled with
10//! `target_feature = "avx"`, which no stock build and no rextendr build does,
11//! so it would silently degrade to two SSE2 halves. `#[target_feature]`
12//! compiles the body unconditionally and is safe to enter behind
13//! `is_x86_feature_detected!`, which is what `detect_simd_level` provides.
14//!
15//! The fused argmin is the exception: it needs compare-and-blend, which is the
16//! part of `wide`'s surface that has churned across minor releases (`CmpLt`
17//! deprecated in 1.5, `blend` in 1.6), and pinning that down across the range
18//! of versions a downstream lockfile might resolve is not worth it for one
19//! kernel. All of its arms, including 128-bit, are written per-architecture
20//! instead. The reductions only need arithmetic and `reduce_add`, which are
21//! stable, so they stay on `wide`.
22//!
23//! The widening reductions at the bottom, which read `f32` and accumulate in
24//! `f64`, are the other exception: `wide` has no `f32x4` to `f64x2`
25//! conversion, so their 128-bit arms are per-architecture too.
26//!
27//! Every hand-written arm follows the same shape, so the two function-local
28//! constants mean the same thing throughout: `W` is the lane count forced by
29//! the intrinsic width (4, 8 or 16 for `f32`; 2, 4 or 8 for `f64`) and
30//! `BLOCK = W * UNROLL` is what one pass of the unrolled loop consumes. Each
31//! arm runs the block loop, folds its accumulators, sweeps any remaining whole
32//! vectors, then finishes the last `< W` elements scalar.
33
34use std::sync::OnceLock;
35use wide::{f32x4, f64x2};
36
37#[cfg(target_arch = "aarch64")]
38use std::arch::aarch64::*;
39#[cfg(target_arch = "x86_64")]
40use std::arch::x86_64::*;
41
42/// Independent accumulators carried through the vectorised loops.
43///
44/// Both `add` and `fma` have roughly four cycles of latency against a
45/// two-per-cycle issue rate, and the argmin's compare-and-blend chain has the
46/// same shape. A single accumulator serialises on that latency and leaves most
47/// of the issue width idle regardless of vector width. Four covers it; more
48/// only lengthens the final cross-accumulator fold.
49///
50/// Indicative, from an A/B of `argmin_diff_simd_f32` on one aarch64 machine:
51/// 1.4x at `len` 200 rising to 2.9x by `len` 16384, the gain growing with
52/// length because the fixed fold cost amortises. Retune by flipping this
53/// constant and timing the same call, not by reasoning about the number.
54pub const UNROLL: usize = 4;
55
56/// Enum for the different architectures and potential SIMD levels
57#[derive(Clone, Copy, Debug)]
58pub enum SimdLevel {
59    /// Scalar version
60    Scalar,
61    /// 128-bit (also covers NEON which is used by Apple)
62    Sse,
63    /// 256-bit
64    Avx2,
65    /// 512-bit
66    Avx512,
67}
68
69/// SIMD level static generated once
70static SIMD_LEVEL: OnceLock<SimdLevel> = OnceLock::new();
71
72/// Function to detect which SIMD implementation to use
73///
74/// The AVX-2 probe also requires FMA, because every 256-bit arm here is
75/// written with `_mm256_fmadd_ps` and is entered under
76/// `#[target_feature(enable = "avx2", enable = "fma")]`. No shipping x86 part
77/// has AVX-2 without FMA, so this costs nothing in practice.
78///
79/// ### Returns
80///
81/// The widest SIMD level this CPU supports.
82pub fn detect_simd_level() -> SimdLevel {
83    *SIMD_LEVEL.get_or_init(|| {
84        #[cfg(target_arch = "x86_64")]
85        {
86            if is_x86_feature_detected!("avx512f") {
87                return SimdLevel::Avx512;
88            }
89            if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
90                return SimdLevel::Avx2;
91            }
92            if is_x86_feature_detected!("sse4.1") {
93                return SimdLevel::Sse;
94            }
95            return SimdLevel::Scalar;
96        }
97
98        #[cfg(target_arch = "aarch64")]
99        {
100            // NEON is always available on aarch64
101            SimdLevel::Sse
102        }
103
104        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
105        {
106            SimdLevel::Scalar
107        }
108    })
109}
110
111/////////////////
112// x86 helpers //
113/////////////////
114
115/// Horizontal sum of a 256-bit f32 vector.
116///
117/// Spilling to the stack rather than shuffling: this runs once per call, so
118/// the shuffle sequence would buy nothing measurable and costs readability.
119///
120/// ### Params
121///
122/// * `v` - The vector to reduce.
123///
124/// ### Returns
125///
126/// The sum of the eight lanes.
127#[cfg(target_arch = "x86_64")]
128#[target_feature(enable = "avx")]
129#[inline]
130unsafe fn hsum_avx_f32(v: __m256) -> f32 {
131    unsafe {
132        let mut tmp = [0.0f32; 8];
133        _mm256_storeu_ps(tmp.as_mut_ptr(), v);
134        tmp.iter().sum()
135    }
136}
137
138/// Horizontal sum of a 256-bit f64 vector.
139///
140/// ### Params
141///
142/// * `v` - The vector to reduce.
143///
144/// ### Returns
145///
146/// The sum of the four lanes.
147#[cfg(target_arch = "x86_64")]
148#[target_feature(enable = "avx")]
149#[inline]
150pub unsafe fn hsum_avx_f64(v: __m256d) -> f64 {
151    unsafe {
152        let mut tmp = [0.0f64; 4];
153        _mm256_storeu_pd(tmp.as_mut_ptr(), v);
154        tmp.iter().sum()
155    }
156}
157
158/// Horizontal sum of a 128-bit f64 vector.
159///
160/// ### Params
161///
162/// * `v` - The vector to reduce.
163///
164/// ### Returns
165///
166/// The sum of the two lanes.
167#[cfg(target_arch = "x86_64")]
168#[target_feature(enable = "sse2")]
169#[inline]
170pub unsafe fn hsum_sse_f64(v: __m128d) -> f64 {
171    unsafe {
172        let mut tmp = [0.0f64; 2];
173        _mm_storeu_pd(tmp.as_mut_ptr(), v);
174        tmp[0] + tmp[1]
175    }
176}
177
178//////////////////////////////////
179// f32-specific implementations //
180//////////////////////////////////
181
182/////////////////
183// Sum squares //
184/////////////////
185
186/// SIMD squared sum of a slice of f32 (scalar)
187///
188/// ### Params
189///
190/// * `a` - The slice of f32 values to sum.
191///
192/// ### Returns
193///
194/// Squared sum
195#[inline(always)]
196fn sum_squares_scalar_f32(a: &[f32]) -> f32 {
197    a.iter().map(|&x| x * x).sum()
198}
199
200/// SIMD squared sum of a slice of f32 (128-bit)
201///
202/// ### Params
203///
204/// * `a` - The slice of f32 values to sum.
205///
206/// ### Returns
207///
208/// Squared sum
209#[inline(always)]
210fn sum_squares_sse_f32(a: &[f32]) -> f32 {
211    const W: usize = 4;
212    const BLOCK: usize = W * UNROLL;
213
214    let len = a.len();
215    let mut acc = [f32x4::ZERO; UNROLL];
216
217    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
218    // or below `len - W`.
219    unsafe {
220        let a_ptr = a.as_ptr();
221
222        let n_blocks = len / BLOCK;
223        for i in 0..n_blocks {
224            let base = i * BLOCK;
225            for (u, acc) in acc.iter_mut().enumerate() {
226                let off = base + u * W;
227                let va = f32x4::from(*(a_ptr.add(off) as *const [f32; W]));
228                *acc += va * va;
229            }
230        }
231
232        let mut total = acc[0];
233        for acc in &acc[1..] {
234            total += *acc;
235        }
236
237        let mut i = n_blocks * BLOCK;
238        while i + W <= len {
239            let va = f32x4::from(*(a_ptr.add(i) as *const [f32; W]));
240            total += va * va;
241            i += W;
242        }
243
244        let mut sum = total.reduce_add();
245        while i < len {
246            sum += a[i] * a[i];
247            i += 1;
248        }
249        sum
250    }
251}
252
253/// SIMD squared sum of a slice of f32 (256-bit)
254///
255/// ### Params
256///
257/// * `a` - The slice of f32 values to sum.
258///
259/// ### Returns
260///
261/// Squared sum
262#[cfg(target_arch = "x86_64")]
263#[target_feature(enable = "avx2", enable = "fma")]
264unsafe fn sum_squares_avx2_f32(a: &[f32]) -> f32 {
265    const W: usize = 8;
266    const BLOCK: usize = W * UNROLL;
267
268    unsafe {
269        let len = a.len();
270        let a_ptr = a.as_ptr();
271        let mut acc = [_mm256_setzero_ps(); UNROLL];
272
273        let n_blocks = len / BLOCK;
274        for i in 0..n_blocks {
275            let base = i * BLOCK;
276            for (u, acc) in acc.iter_mut().enumerate() {
277                let va = _mm256_loadu_ps(a_ptr.add(base + u * W));
278                *acc = _mm256_fmadd_ps(va, va, *acc);
279            }
280        }
281
282        let mut total = acc[0];
283        for acc in &acc[1..] {
284            total = _mm256_add_ps(total, *acc);
285        }
286
287        let mut i = n_blocks * BLOCK;
288        while i + W <= len {
289            let va = _mm256_loadu_ps(a_ptr.add(i));
290            total = _mm256_fmadd_ps(va, va, total);
291            i += W;
292        }
293
294        let mut sum = hsum_avx_f32(total);
295        while i < len {
296            sum += a[i] * a[i];
297            i += 1;
298        }
299        sum
300    }
301}
302
303/// SIMD squared sum of a slice of f32 (512-bit)
304///
305/// ### Params
306///
307/// * `a` - The slice of f32 values to sum.
308///
309/// ### Returns
310///
311/// Squared sum
312#[cfg(target_arch = "x86_64")]
313#[target_feature(enable = "avx512f")]
314unsafe fn sum_squares_avx512_f32(a: &[f32]) -> f32 {
315    const W: usize = 16;
316    const BLOCK: usize = W * UNROLL;
317
318    unsafe {
319        let len = a.len();
320        let a_ptr = a.as_ptr();
321        let mut acc = [_mm512_setzero_ps(); UNROLL];
322
323        let n_blocks = len / BLOCK;
324        for i in 0..n_blocks {
325            let base = i * BLOCK;
326            for (u, acc) in acc.iter_mut().enumerate() {
327                let va = _mm512_loadu_ps(a_ptr.add(base + u * W));
328                *acc = _mm512_fmadd_ps(va, va, *acc);
329            }
330        }
331
332        let mut total = acc[0];
333        for acc in &acc[1..] {
334            total = _mm512_add_ps(total, *acc);
335        }
336
337        let mut i = n_blocks * BLOCK;
338        while i + W <= len {
339            let va = _mm512_loadu_ps(a_ptr.add(i));
340            total = _mm512_fmadd_ps(va, va, total);
341            i += W;
342        }
343
344        let mut sum = _mm512_reduce_add_ps(total);
345        while i < len {
346            sum += a[i] * a[i];
347            i += 1;
348        }
349        sum
350    }
351}
352
353/// SIMD squared sum of a slice of f32 (dispatch)
354///
355/// ### Params
356///
357/// * `a` - The slice of f32 values to sum.
358///
359/// ### Returns
360///
361/// Squared sum
362#[inline]
363pub fn sum_squares_simd_f32(a: &[f32]) -> f32 {
364    #[cfg(target_arch = "x86_64")]
365    // SAFETY: each arm is entered only once `detect_simd_level` has confirmed
366    // the CPU reports the features that arm is compiled for.
367    unsafe {
368        match detect_simd_level() {
369            SimdLevel::Avx512 => sum_squares_avx512_f32(a),
370            SimdLevel::Avx2 => sum_squares_avx2_f32(a),
371            SimdLevel::Sse => sum_squares_sse_f32(a),
372            SimdLevel::Scalar => sum_squares_scalar_f32(a),
373        }
374    }
375
376    #[cfg(not(target_arch = "x86_64"))]
377    match detect_simd_level() {
378        SimdLevel::Sse => sum_squares_sse_f32(a),
379        _ => sum_squares_scalar_f32(a),
380    }
381}
382
383//////////////////////
384// General versions //
385//////////////////////
386
387/// Trait for SIMD-accelerated dot product, replacing the ann-search-rs
388/// SimdDistance dependency.
389pub trait BixverseSimd:
390    Copy + Default + std::ops::AddAssign + std::ops::Mul<Output = Self>
391{
392    /// Compute the dot product of two slices using SIMD where available.
393    ///
394    /// ### Params
395    ///
396    /// * `a` - First slice
397    /// * `b` - Second slice (must be the same length as `a`)
398    ///
399    /// ### Returns
400    ///
401    /// The dot product of `a` and `b`
402    fn bxv_dot_simd(a: &[Self], b: &[Self]) -> Self;
403
404    /// Compute the sum over a slice with SIMD
405    ///
406    /// ### Params
407    ///
408    /// * `a` - Slice for which to calculate the sum
409    ///
410    /// ### Returns
411    ///
412    /// The sum of the vector
413    fn bxv_sum(x: &[Self]) -> Self;
414
415    /// Sum of squared deviations from the mean
416    ///
417    /// ### Params
418    ///
419    /// * `x` - Vector for which to calculate the squared deviations from the
420    ///   mean.
421    /// * `mean` - The mean.
422    ///
423    /// ### Returns
424    ///
425    /// The sum of the squared deviations.
426    fn bxv_sum_squared_deviation(x: &[Self], mean: Self) -> Self;
427
428    /// Scaled vector addition, `y += a * x`, in place.
429    ///
430    /// The inner step of any sparse-operator-times-dense-block kernel, where
431    /// `y` is one contiguous output row and `x` one contiguous input row.
432    ///
433    /// Defaulted to a scalar loop so that adding it did not break the trait for
434    /// out-of-crate implementors. Both in-crate impls override it.
435    ///
436    /// ### Params
437    ///
438    /// * `y` - Accumulator, updated in place.
439    /// * `a` - Scalar multiplier.
440    /// * `x` - Slice added into `y` (must be the same length as `y`).
441    fn bxv_axpy_simd(y: &mut [Self], a: Self, x: &[Self]) {
442        for (y_i, &x_i) in y.iter_mut().zip(x.iter()) {
443            *y_i += a * x_i;
444        }
445    }
446
447    /// Shifted exponential, `dst = exp(src - shift)`, returning `sum(dst)`.
448    ///
449    /// The inner step of a stable softmax, with `shift` the row maximum.
450    ///
451    /// ### Params
452    ///
453    /// * `src` - Input slice
454    /// * `shift` - Subtracted from every element before the exponential
455    /// * `dst` - Output, same length as `src`
456    ///
457    /// ### Returns
458    ///
459    /// The sum of `dst`
460    fn bxv_exp_shift_sum(src: &[Self], shift: Self, dst: &mut [Self]) -> Self;
461}
462
463impl BixverseSimd for f32 {
464    #[inline]
465    fn bxv_dot_simd(a: &[f32], b: &[f32]) -> f32 {
466        dot_simd_f32(a, b)
467    }
468
469    #[inline]
470    fn bxv_sum(x: &[f32]) -> f32 {
471        sum_simd_f32(x)
472    }
473
474    #[inline]
475    fn bxv_sum_squared_deviation(x: &[f32], mean: f32) -> f32 {
476        sum_squared_dev_simd_f32(x, mean)
477    }
478
479    #[inline]
480    fn bxv_axpy_simd(y: &mut [f32], a: f32, x: &[f32]) {
481        axpy_simd_f32(y, a, x)
482    }
483
484    #[inline]
485    fn bxv_exp_shift_sum(src: &[f32], shift: f32, dst: &mut [f32]) -> f32 {
486        exp_shift_sum_simd_f32(src, shift, dst)
487    }
488}
489
490impl BixverseSimd for f64 {
491    #[inline]
492    fn bxv_dot_simd(a: &[f64], b: &[f64]) -> f64 {
493        dot_simd_f64(a, b)
494    }
495
496    #[inline]
497    fn bxv_sum(x: &[f64]) -> f64 {
498        sum_simd_f64(x)
499    }
500
501    #[inline]
502    fn bxv_sum_squared_deviation(x: &[f64], mean: f64) -> f64 {
503        sum_squared_dev_simd_f64(x, mean)
504    }
505
506    #[inline]
507    fn bxv_axpy_simd(y: &mut [f64], a: f64, x: &[f64]) {
508        axpy_simd_f64(y, a, x)
509    }
510
511    #[inline]
512    fn bxv_exp_shift_sum(src: &[f64], shift: f64, dst: &mut [f64]) -> f64 {
513        exp_shift_sum_scalar(src, shift, dst)
514    }
515}
516
517//////////////////
518// Dot products //
519//////////////////
520
521/////////
522// f32 //
523/////////
524
525/// SIMD dot product of two slices of f32 (scalar)
526///
527/// ### Params
528///
529/// * `a` - The first slice of f32 values.
530/// * `b` - The second slice of f32 values.
531///
532/// ### Returns
533///
534/// Dot product
535#[inline(always)]
536fn dot_scalar_f32(a: &[f32], b: &[f32]) -> f32 {
537    a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
538}
539
540/// SIMD dot product of two slices of f32 (128-bit)
541///
542/// ### Params
543///
544/// * `a` - The first slice of f32 values.
545/// * `b` - The second slice of f32 values.
546///
547/// ### Returns
548///
549/// Dot product
550#[inline(always)]
551fn dot_sse_f32(a: &[f32], b: &[f32]) -> f32 {
552    const W: usize = 4;
553    const BLOCK: usize = W * UNROLL;
554
555    let len = a.len();
556    let mut acc = [f32x4::ZERO; UNROLL];
557
558    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
559    // or below `len - W`.
560    unsafe {
561        let a_ptr = a.as_ptr();
562        let b_ptr = b.as_ptr();
563
564        let n_blocks = len / BLOCK;
565        for i in 0..n_blocks {
566            let base = i * BLOCK;
567            for (u, acc) in acc.iter_mut().enumerate() {
568                let off = base + u * W;
569                let va = f32x4::from(*(a_ptr.add(off) as *const [f32; W]));
570                let vb = f32x4::from(*(b_ptr.add(off) as *const [f32; W]));
571                *acc += va * vb;
572            }
573        }
574
575        let mut total = acc[0];
576        for acc in &acc[1..] {
577            total += *acc;
578        }
579
580        let mut i = n_blocks * BLOCK;
581        while i + W <= len {
582            let va = f32x4::from(*(a_ptr.add(i) as *const [f32; W]));
583            let vb = f32x4::from(*(b_ptr.add(i) as *const [f32; W]));
584            total += va * vb;
585            i += W;
586        }
587
588        let mut sum = total.reduce_add();
589        while i < len {
590            sum += a[i] * b[i];
591            i += 1;
592        }
593        sum
594    }
595}
596
597/// SIMD dot product of two slices of f32 (256-bit)
598///
599/// ### Params
600///
601/// * `a` - The first slice of f32 values.
602/// * `b` - The second slice of f32 values.
603///
604/// ### Returns
605///
606/// Dot product
607#[cfg(target_arch = "x86_64")]
608#[target_feature(enable = "avx2", enable = "fma")]
609unsafe fn dot_avx2_f32(a: &[f32], b: &[f32]) -> f32 {
610    const W: usize = 8;
611    const BLOCK: usize = W * UNROLL;
612
613    unsafe {
614        let len = a.len();
615        let a_ptr = a.as_ptr();
616        let b_ptr = b.as_ptr();
617        let mut acc = [_mm256_setzero_ps(); UNROLL];
618
619        let n_blocks = len / BLOCK;
620        for i in 0..n_blocks {
621            let base = i * BLOCK;
622            for (u, acc) in acc.iter_mut().enumerate() {
623                let off = base + u * W;
624                let va = _mm256_loadu_ps(a_ptr.add(off));
625                let vb = _mm256_loadu_ps(b_ptr.add(off));
626                *acc = _mm256_fmadd_ps(va, vb, *acc);
627            }
628        }
629
630        let mut total = acc[0];
631        for acc in &acc[1..] {
632            total = _mm256_add_ps(total, *acc);
633        }
634
635        let mut i = n_blocks * BLOCK;
636        while i + W <= len {
637            let va = _mm256_loadu_ps(a_ptr.add(i));
638            let vb = _mm256_loadu_ps(b_ptr.add(i));
639            total = _mm256_fmadd_ps(va, vb, total);
640            i += W;
641        }
642
643        let mut sum = hsum_avx_f32(total);
644        while i < len {
645            sum += a[i] * b[i];
646            i += 1;
647        }
648        sum
649    }
650}
651
652/// SIMD dot product of two slices of f32 (512-bit)
653///
654/// ### Params
655///
656/// * `a` - The first slice of f32 values.
657/// * `b` - The second slice of f32 values.
658///
659/// ### Returns
660///
661/// Dot product
662#[cfg(target_arch = "x86_64")]
663#[target_feature(enable = "avx512f")]
664unsafe fn dot_avx512_f32(a: &[f32], b: &[f32]) -> f32 {
665    const W: usize = 16;
666    const BLOCK: usize = W * UNROLL;
667
668    unsafe {
669        let len = a.len();
670        let a_ptr = a.as_ptr();
671        let b_ptr = b.as_ptr();
672        let mut acc = [_mm512_setzero_ps(); UNROLL];
673
674        let n_blocks = len / BLOCK;
675        for i in 0..n_blocks {
676            let base = i * BLOCK;
677            for (u, acc) in acc.iter_mut().enumerate() {
678                let off = base + u * W;
679                let va = _mm512_loadu_ps(a_ptr.add(off));
680                let vb = _mm512_loadu_ps(b_ptr.add(off));
681                *acc = _mm512_fmadd_ps(va, vb, *acc);
682            }
683        }
684
685        let mut total = acc[0];
686        for acc in &acc[1..] {
687            total = _mm512_add_ps(total, *acc);
688        }
689
690        let mut i = n_blocks * BLOCK;
691        while i + W <= len {
692            let va = _mm512_loadu_ps(a_ptr.add(i));
693            let vb = _mm512_loadu_ps(b_ptr.add(i));
694            total = _mm512_fmadd_ps(va, vb, total);
695            i += W;
696        }
697
698        let mut sum = _mm512_reduce_add_ps(total);
699        while i < len {
700            sum += a[i] * b[i];
701            i += 1;
702        }
703        sum
704    }
705}
706
707/// SIMD dot product of two slices of f32 (dispatch)
708///
709/// Dispatches to the best available SIMD implementation at runtime.
710///
711/// ### Params
712///
713/// * `a` - The first slice of f32 values.
714/// * `b` - The second slice of f32 values.
715///
716/// ### Returns
717///
718/// Dot product
719#[inline]
720pub fn dot_simd_f32(a: &[f32], b: &[f32]) -> f32 {
721    #[cfg(target_arch = "x86_64")]
722    // SAFETY: see `sum_squares_simd_f32`.
723    unsafe {
724        match detect_simd_level() {
725            SimdLevel::Avx512 => dot_avx512_f32(a, b),
726            SimdLevel::Avx2 => dot_avx2_f32(a, b),
727            SimdLevel::Sse => dot_sse_f32(a, b),
728            SimdLevel::Scalar => dot_scalar_f32(a, b),
729        }
730    }
731
732    #[cfg(not(target_arch = "x86_64"))]
733    match detect_simd_level() {
734        SimdLevel::Sse => dot_sse_f32(a, b),
735        _ => dot_scalar_f32(a, b),
736    }
737}
738
739/////////
740// f64 //
741/////////
742
743/// SIMD dot product of two slices of f64 (scalar)
744///
745/// ### Params
746///
747/// * `a` - The first slice of f64 values.
748/// * `b` - The second slice of f64 values.
749///
750/// ### Returns
751///
752/// Dot product
753#[inline(always)]
754fn dot_scalar_f64(a: &[f64], b: &[f64]) -> f64 {
755    a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
756}
757
758/// SIMD dot product of two slices of f64 (128-bit)
759///
760/// ### Params
761///
762/// * `a` - The first slice of f64 values.
763/// * `b` - The second slice of f64 values.
764///
765/// ### Returns
766///
767/// Dot product
768#[inline(always)]
769fn dot_sse_f64(a: &[f64], b: &[f64]) -> f64 {
770    const W: usize = 2;
771    const BLOCK: usize = W * UNROLL;
772
773    let len = a.len();
774    let mut acc = [f64x2::ZERO; UNROLL];
775
776    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
777    // or below `len - W`.
778    unsafe {
779        let a_ptr = a.as_ptr();
780        let b_ptr = b.as_ptr();
781
782        let n_blocks = len / BLOCK;
783        for i in 0..n_blocks {
784            let base = i * BLOCK;
785            for (u, acc) in acc.iter_mut().enumerate() {
786                let off = base + u * W;
787                let va = f64x2::from(*(a_ptr.add(off) as *const [f64; W]));
788                let vb = f64x2::from(*(b_ptr.add(off) as *const [f64; W]));
789                *acc += va * vb;
790            }
791        }
792
793        let mut total = acc[0];
794        for acc in &acc[1..] {
795            total += *acc;
796        }
797
798        let mut i = n_blocks * BLOCK;
799        while i + W <= len {
800            let va = f64x2::from(*(a_ptr.add(i) as *const [f64; W]));
801            let vb = f64x2::from(*(b_ptr.add(i) as *const [f64; W]));
802            total += va * vb;
803            i += W;
804        }
805
806        let mut sum = total.reduce_add();
807        while i < len {
808            sum += a[i] * b[i];
809            i += 1;
810        }
811        sum
812    }
813}
814
815/// SIMD dot product of two slices of f64 (256-bit)
816///
817/// ### Params
818///
819/// * `a` - The first slice of f64 values.
820/// * `b` - The second slice of f64 values.
821///
822/// ### Returns
823///
824/// Dot product
825#[cfg(target_arch = "x86_64")]
826#[target_feature(enable = "avx2", enable = "fma")]
827unsafe fn dot_avx2_f64(a: &[f64], b: &[f64]) -> f64 {
828    const W: usize = 4;
829    const BLOCK: usize = W * UNROLL;
830
831    unsafe {
832        let len = a.len();
833        let a_ptr = a.as_ptr();
834        let b_ptr = b.as_ptr();
835        let mut acc = [_mm256_setzero_pd(); UNROLL];
836
837        let n_blocks = len / BLOCK;
838        for i in 0..n_blocks {
839            let base = i * BLOCK;
840            for (u, acc) in acc.iter_mut().enumerate() {
841                let off = base + u * W;
842                let va = _mm256_loadu_pd(a_ptr.add(off));
843                let vb = _mm256_loadu_pd(b_ptr.add(off));
844                *acc = _mm256_fmadd_pd(va, vb, *acc);
845            }
846        }
847
848        let mut total = acc[0];
849        for acc in &acc[1..] {
850            total = _mm256_add_pd(total, *acc);
851        }
852
853        let mut i = n_blocks * BLOCK;
854        while i + W <= len {
855            let va = _mm256_loadu_pd(a_ptr.add(i));
856            let vb = _mm256_loadu_pd(b_ptr.add(i));
857            total = _mm256_fmadd_pd(va, vb, total);
858            i += W;
859        }
860
861        let mut sum = hsum_avx_f64(total);
862        while i < len {
863            sum += a[i] * b[i];
864            i += 1;
865        }
866        sum
867    }
868}
869
870/// SIMD dot product of two slices of f64 (512-bit)
871///
872/// ### Params
873///
874/// * `a` - The first slice of f64 values.
875/// * `b` - The second slice of f64 values.
876///
877/// ### Returns
878///
879/// Dot product
880#[cfg(target_arch = "x86_64")]
881#[target_feature(enable = "avx512f")]
882unsafe fn dot_avx512_f64(a: &[f64], b: &[f64]) -> f64 {
883    const W: usize = 8;
884    const BLOCK: usize = W * UNROLL;
885
886    unsafe {
887        let len = a.len();
888        let a_ptr = a.as_ptr();
889        let b_ptr = b.as_ptr();
890        let mut acc = [_mm512_setzero_pd(); UNROLL];
891
892        let n_blocks = len / BLOCK;
893        for i in 0..n_blocks {
894            let base = i * BLOCK;
895            for (u, acc) in acc.iter_mut().enumerate() {
896                let off = base + u * W;
897                let va = _mm512_loadu_pd(a_ptr.add(off));
898                let vb = _mm512_loadu_pd(b_ptr.add(off));
899                *acc = _mm512_fmadd_pd(va, vb, *acc);
900            }
901        }
902
903        let mut total = acc[0];
904        for acc in &acc[1..] {
905            total = _mm512_add_pd(total, *acc);
906        }
907
908        let mut i = n_blocks * BLOCK;
909        while i + W <= len {
910            let va = _mm512_loadu_pd(a_ptr.add(i));
911            let vb = _mm512_loadu_pd(b_ptr.add(i));
912            total = _mm512_fmadd_pd(va, vb, total);
913            i += W;
914        }
915
916        let mut sum = _mm512_reduce_add_pd(total);
917        while i < len {
918            sum += a[i] * b[i];
919            i += 1;
920        }
921        sum
922    }
923}
924
925/// SIMD dot product of two slices of f64 (dispatch)
926///
927/// Dispatches to the best available SIMD implementation at runtime.
928///
929/// ### Params
930///
931/// * `a` - The first slice of f64 values.
932/// * `b` - The second slice of f64 values.
933///
934/// ### Returns
935///
936/// Dot product
937#[inline]
938pub fn dot_simd_f64(a: &[f64], b: &[f64]) -> f64 {
939    #[cfg(target_arch = "x86_64")]
940    // SAFETY: see `sum_squares_simd_f32`.
941    unsafe {
942        match detect_simd_level() {
943            SimdLevel::Avx512 => dot_avx512_f64(a, b),
944            SimdLevel::Avx2 => dot_avx2_f64(a, b),
945            SimdLevel::Sse => dot_sse_f64(a, b),
946            SimdLevel::Scalar => dot_scalar_f64(a, b),
947        }
948    }
949
950    #[cfg(not(target_arch = "x86_64"))]
951    match detect_simd_level() {
952        SimdLevel::Sse => dot_sse_f64(a, b),
953        _ => dot_scalar_f64(a, b),
954    }
955}
956
957///////////////////////////
958// Fused subtract-argmin //
959///////////////////////////
960
961/// Fused subtract-and-argmin over two slices of f32 (scalar)
962///
963/// ### Params
964///
965/// * `a` - The minuend slice.
966/// * `b` - The subtrahend slice, same length as `a`.
967///
968/// ### Returns
969///
970/// `(index, value)` of the smallest `a[i] - b[i]`, ties to the lowest index.
971#[inline(always)]
972fn argmin_diff_scalar_f32(a: &[f32], b: &[f32]) -> (usize, f32) {
973    let mut min_val = a[0] - b[0];
974    let mut min_idx = 0usize;
975    for i in 1..a.len() {
976        let diff = a[i] - b[i];
977        if diff < min_val {
978            min_val = diff;
979            min_idx = i;
980        }
981    }
982    (min_idx, min_val)
983}
984
985/// Reduce per-lane running minima to a single `(index, value)`.
986///
987/// Lane indices are carried as `f32`, which is exact for any index below 2^24
988/// and avoids a bitcast between the float compare mask and an integer vector.
989///
990/// ### Params
991///
992/// * `vals` - Per-lane minimum values.
993/// * `idxs` - Per-lane indices at which those minima occurred.
994///
995/// ### Returns
996///
997/// `(index, value)` of the smallest entry, ties to the lowest index.
998#[inline(always)]
999fn reduce_lane_argmin_f32(vals: &[f32], idxs: &[f32]) -> (usize, f32) {
1000    let mut min_val = f32::INFINITY;
1001    let mut min_idx = usize::MAX;
1002    for (&value, &index) in vals.iter().zip(idxs.iter()) {
1003        let index = index as usize;
1004        if value < min_val || (value == min_val && index < min_idx) {
1005            min_val = value;
1006            min_idx = index;
1007        }
1008    }
1009    (min_idx, min_val)
1010}
1011
1012/// Merge one accumulator pair into another, lane by lane (128-bit, SSE4.1).
1013///
1014/// The tie-break has to compare indices explicitly rather than rely on
1015/// accumulator order. Within one accumulator indices grow monotonically, so a
1016/// strict `<` in the scan keeps the earliest. Across accumulators that no
1017/// longer holds: accumulator 1's minimum may come from an earlier block than
1018/// accumulator 0's, so its index can be the lower of the two.
1019///
1020/// ### Params
1021///
1022/// * `best` - Running per-lane minima.
1023/// * `best_idx` - Indices of those minima.
1024/// * `cand` - Candidate per-lane minima to merge in.
1025/// * `cand_idx` - Indices of the candidates.
1026///
1027/// ### Returns
1028///
1029/// The merged `(values, indices)` pair, ties to the lower index.
1030#[cfg(target_arch = "x86_64")]
1031#[target_feature(enable = "sse4.1")]
1032#[inline]
1033unsafe fn fold_argmin_sse(
1034    best: __m128,
1035    best_idx: __m128,
1036    cand: __m128,
1037    cand_idx: __m128,
1038) -> (__m128, __m128) {
1039    let lt = _mm_cmplt_ps(cand, best);
1040    let eq = _mm_cmpeq_ps(cand, best);
1041    let ilt = _mm_cmplt_ps(cand_idx, best_idx);
1042    let take = _mm_or_ps(lt, _mm_and_ps(eq, ilt));
1043    (
1044        _mm_blendv_ps(best, cand, take),
1045        _mm_blendv_ps(best_idx, cand_idx, take),
1046    )
1047}
1048
1049/// Fused subtract-and-argmin over two slices of f32 (128-bit, SSE4.1)
1050///
1051/// ### Params
1052///
1053/// * `a` - The minuend slice.
1054/// * `b` - The subtrahend slice, same length as `a`.
1055///
1056/// ### Returns
1057///
1058/// `(index, value)` of the smallest `a[i] - b[i]`, ties to the lowest index.
1059#[cfg(target_arch = "x86_64")]
1060#[target_feature(enable = "sse4.1")]
1061unsafe fn argmin_diff_sse_f32(a: &[f32], b: &[f32]) -> (usize, f32) {
1062    const W: usize = 4;
1063    const BLOCK: usize = W * UNROLL;
1064
1065    unsafe {
1066        let len = a.len();
1067        let a_ptr = a.as_ptr();
1068        let b_ptr = b.as_ptr();
1069        let n_blocks = len / BLOCK;
1070
1071        let mut best = [_mm_set1_ps(f32::INFINITY); UNROLL];
1072        let mut best_idx = [_mm_setzero_ps(); UNROLL];
1073        let lane_base = _mm_setr_ps(0.0, 1.0, 2.0, 3.0);
1074        let mut lane: [__m128; UNROLL] =
1075            std::array::from_fn(|u| _mm_add_ps(lane_base, _mm_set1_ps((u * W) as f32)));
1076        let step = _mm_set1_ps(BLOCK as f32);
1077
1078        for i in 0..n_blocks {
1079            let base = i * BLOCK;
1080            for u in 0..UNROLL {
1081                let off = base + u * W;
1082                let diff = _mm_sub_ps(_mm_loadu_ps(a_ptr.add(off)), _mm_loadu_ps(b_ptr.add(off)));
1083                // Strict `<` keeps the earliest block within a lane; ties
1084                // between accumulators are resolved by `fold_argmin_sse`.
1085                let mask = _mm_cmplt_ps(diff, best[u]);
1086                best[u] = _mm_blendv_ps(best[u], diff, mask);
1087                best_idx[u] = _mm_blendv_ps(best_idx[u], lane[u], mask);
1088                lane[u] = _mm_add_ps(lane[u], step);
1089            }
1090        }
1091
1092        let mut acc = best[0];
1093        let mut acc_idx = best_idx[0];
1094        for u in 1..UNROLL {
1095            (acc, acc_idx) = fold_argmin_sse(acc, acc_idx, best[u], best_idx[u]);
1096        }
1097
1098        // Whole vectors left over by the block loop. Their indices are strictly
1099        // greater than anything already in `acc`, so a strict `<` still keeps
1100        // the earliest on a tie.
1101        let mut i = n_blocks * BLOCK;
1102        while i + W <= len {
1103            let diff = _mm_sub_ps(_mm_loadu_ps(a_ptr.add(i)), _mm_loadu_ps(b_ptr.add(i)));
1104            let lane_v = _mm_add_ps(lane_base, _mm_set1_ps(i as f32));
1105            let mask = _mm_cmplt_ps(diff, acc);
1106            acc = _mm_blendv_ps(acc, diff, mask);
1107            acc_idx = _mm_blendv_ps(acc_idx, lane_v, mask);
1108            i += W;
1109        }
1110
1111        let mut vals = [0.0f32; W];
1112        let mut idxs = [0.0f32; W];
1113        _mm_storeu_ps(vals.as_mut_ptr(), acc);
1114        _mm_storeu_ps(idxs.as_mut_ptr(), acc_idx);
1115        let (mut min_idx, mut min_val) = reduce_lane_argmin_f32(&vals, &idxs);
1116
1117        while i < len {
1118            let diff = a[i] - b[i];
1119            if diff < min_val {
1120                min_val = diff;
1121                min_idx = i;
1122            }
1123            i += 1;
1124        }
1125
1126        (min_idx, min_val)
1127    }
1128}
1129
1130/// Merge one accumulator pair into another, lane by lane (NEON).
1131///
1132/// See `fold_argmin_sse` for why the index comparison is needed.
1133///
1134/// ### Params
1135///
1136/// * `best` - Running per-lane minima.
1137/// * `best_idx` - Indices of those minima.
1138/// * `cand` - Candidate per-lane minima to merge in.
1139/// * `cand_idx` - Indices of the candidates.
1140///
1141/// ### Returns
1142///
1143/// The merged `(values, indices)` pair, ties to the lower index.
1144#[cfg(target_arch = "aarch64")]
1145#[target_feature(enable = "neon")]
1146#[inline]
1147unsafe fn fold_argmin_neon(
1148    best: float32x4_t,
1149    best_idx: float32x4_t,
1150    cand: float32x4_t,
1151    cand_idx: float32x4_t,
1152) -> (float32x4_t, float32x4_t) {
1153    let lt = vcltq_f32(cand, best);
1154    let eq = vceqq_f32(cand, best);
1155    let ilt = vcltq_f32(cand_idx, best_idx);
1156    let take = vorrq_u32(lt, vandq_u32(eq, ilt));
1157    (
1158        vbslq_f32(take, cand, best),
1159        vbslq_f32(take, cand_idx, best_idx),
1160    )
1161}
1162
1163/// Fused subtract-and-argmin over two slices of f32 (128-bit, NEON)
1164///
1165/// ### Params
1166///
1167/// * `a` - The minuend slice.
1168/// * `b` - The subtrahend slice, same length as `a`.
1169///
1170/// ### Returns
1171///
1172/// `(index, value)` of the smallest `a[i] - b[i]`, ties to the lowest index.
1173#[cfg(target_arch = "aarch64")]
1174#[target_feature(enable = "neon")]
1175unsafe fn argmin_diff_neon_f32(a: &[f32], b: &[f32]) -> (usize, f32) {
1176    const W: usize = 4;
1177    const BLOCK: usize = W * UNROLL;
1178
1179    unsafe {
1180        let len = a.len();
1181        let a_ptr = a.as_ptr();
1182        let b_ptr = b.as_ptr();
1183        let n_blocks = len / BLOCK;
1184
1185        let mut best = [vdupq_n_f32(f32::INFINITY); UNROLL];
1186        let mut best_idx = [vdupq_n_f32(0.0); UNROLL];
1187        let lane_seed: [f32; W] = [0.0, 1.0, 2.0, 3.0];
1188        let lane_base = vld1q_f32(lane_seed.as_ptr());
1189        let mut lane: [float32x4_t; UNROLL] =
1190            std::array::from_fn(|u| vaddq_f32(lane_base, vdupq_n_f32((u * W) as f32)));
1191        let step = vdupq_n_f32(BLOCK as f32);
1192
1193        for i in 0..n_blocks {
1194            let base = i * BLOCK;
1195            for u in 0..UNROLL {
1196                let off = base + u * W;
1197                let diff = vsubq_f32(vld1q_f32(a_ptr.add(off)), vld1q_f32(b_ptr.add(off)));
1198                // Strict `<` keeps the earliest block within a lane; ties
1199                // between accumulators are resolved by `fold_argmin_neon`.
1200                let mask = vcltq_f32(diff, best[u]);
1201                best[u] = vbslq_f32(mask, diff, best[u]);
1202                best_idx[u] = vbslq_f32(mask, lane[u], best_idx[u]);
1203                lane[u] = vaddq_f32(lane[u], step);
1204            }
1205        }
1206
1207        let mut acc = best[0];
1208        let mut acc_idx = best_idx[0];
1209        for u in 1..UNROLL {
1210            (acc, acc_idx) = fold_argmin_neon(acc, acc_idx, best[u], best_idx[u]);
1211        }
1212
1213        // See the SSE arm: leftover whole vectors carry strictly higher
1214        // indices, so the strict `<` is still the right tie-break.
1215        let mut i = n_blocks * BLOCK;
1216        while i + W <= len {
1217            let diff = vsubq_f32(vld1q_f32(a_ptr.add(i)), vld1q_f32(b_ptr.add(i)));
1218            let lane_v = vaddq_f32(lane_base, vdupq_n_f32(i as f32));
1219            let mask = vcltq_f32(diff, acc);
1220            acc = vbslq_f32(mask, diff, acc);
1221            acc_idx = vbslq_f32(mask, lane_v, acc_idx);
1222            i += W;
1223        }
1224
1225        let mut vals = [0.0f32; W];
1226        let mut idxs = [0.0f32; W];
1227        vst1q_f32(vals.as_mut_ptr(), acc);
1228        vst1q_f32(idxs.as_mut_ptr(), acc_idx);
1229        let (mut min_idx, mut min_val) = reduce_lane_argmin_f32(&vals, &idxs);
1230
1231        while i < len {
1232            let diff = a[i] - b[i];
1233            if diff < min_val {
1234                min_val = diff;
1235                min_idx = i;
1236            }
1237            i += 1;
1238        }
1239
1240        (min_idx, min_val)
1241    }
1242}
1243
1244/// Merge one accumulator pair into another, lane by lane (256-bit).
1245///
1246/// See `fold_argmin_sse` for why the index comparison is needed.
1247///
1248/// ### Params
1249///
1250/// * `best` - Running per-lane minima.
1251/// * `best_idx` - Indices of those minima.
1252/// * `cand` - Candidate per-lane minima to merge in.
1253/// * `cand_idx` - Indices of the candidates.
1254///
1255/// ### Returns
1256///
1257/// The merged `(values, indices)` pair, ties to the lower index.
1258#[cfg(target_arch = "x86_64")]
1259#[target_feature(enable = "avx")]
1260#[inline]
1261unsafe fn fold_argmin_avx(
1262    best: __m256,
1263    best_idx: __m256,
1264    cand: __m256,
1265    cand_idx: __m256,
1266) -> (__m256, __m256) {
1267    let lt = _mm256_cmp_ps(cand, best, _CMP_LT_OQ);
1268    let eq = _mm256_cmp_ps(cand, best, _CMP_EQ_OQ);
1269    let ilt = _mm256_cmp_ps(cand_idx, best_idx, _CMP_LT_OQ);
1270    let take = _mm256_or_ps(lt, _mm256_and_ps(eq, ilt));
1271    (
1272        _mm256_blendv_ps(best, cand, take),
1273        _mm256_blendv_ps(best_idx, cand_idx, take),
1274    )
1275}
1276
1277/// Fused subtract-and-argmin over two slices of f32 (256-bit)
1278///
1279/// ### Params
1280///
1281/// * `a` - The minuend slice.
1282/// * `b` - The subtrahend slice, same length as `a`.
1283///
1284/// ### Returns
1285///
1286/// `(index, value)` of the smallest `a[i] - b[i]`, ties to the lowest index.
1287#[cfg(target_arch = "x86_64")]
1288#[target_feature(enable = "avx2", enable = "fma")]
1289unsafe fn argmin_diff_avx2_f32(a: &[f32], b: &[f32]) -> (usize, f32) {
1290    const W: usize = 8;
1291    const BLOCK: usize = W * UNROLL;
1292
1293    unsafe {
1294        let len = a.len();
1295        let a_ptr = a.as_ptr();
1296        let b_ptr = b.as_ptr();
1297        let n_blocks = len / BLOCK;
1298
1299        let mut best = [_mm256_set1_ps(f32::INFINITY); UNROLL];
1300        let mut best_idx = [_mm256_setzero_ps(); UNROLL];
1301        let lane_base = _mm256_setr_ps(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
1302        let mut lane: [__m256; UNROLL] =
1303            std::array::from_fn(|u| _mm256_add_ps(lane_base, _mm256_set1_ps((u * W) as f32)));
1304        let step = _mm256_set1_ps(BLOCK as f32);
1305
1306        for i in 0..n_blocks {
1307            let base = i * BLOCK;
1308            for u in 0..UNROLL {
1309                let off = base + u * W;
1310                let va = _mm256_loadu_ps(a_ptr.add(off));
1311                let vb = _mm256_loadu_ps(b_ptr.add(off));
1312                let diff = _mm256_sub_ps(va, vb);
1313                // Strict `<` keeps the earliest block within a lane; ties
1314                // between accumulators are resolved by `fold_argmin_avx`.
1315                let mask = _mm256_cmp_ps(diff, best[u], _CMP_LT_OQ);
1316                best[u] = _mm256_blendv_ps(best[u], diff, mask);
1317                best_idx[u] = _mm256_blendv_ps(best_idx[u], lane[u], mask);
1318                lane[u] = _mm256_add_ps(lane[u], step);
1319            }
1320        }
1321
1322        let mut acc = best[0];
1323        let mut acc_idx = best_idx[0];
1324        for u in 1..UNROLL {
1325            (acc, acc_idx) = fold_argmin_avx(acc, acc_idx, best[u], best_idx[u]);
1326        }
1327
1328        // See the SSE arm: leftover whole vectors carry strictly higher
1329        // indices, so the strict `<` is still the right tie-break.
1330        let mut i = n_blocks * BLOCK;
1331        while i + W <= len {
1332            let diff = _mm256_sub_ps(_mm256_loadu_ps(a_ptr.add(i)), _mm256_loadu_ps(b_ptr.add(i)));
1333            let lane_v = _mm256_add_ps(lane_base, _mm256_set1_ps(i as f32));
1334            let mask = _mm256_cmp_ps(diff, acc, _CMP_LT_OQ);
1335            acc = _mm256_blendv_ps(acc, diff, mask);
1336            acc_idx = _mm256_blendv_ps(acc_idx, lane_v, mask);
1337            i += W;
1338        }
1339
1340        let mut vals = [0.0f32; W];
1341        let mut idxs = [0.0f32; W];
1342        _mm256_storeu_ps(vals.as_mut_ptr(), acc);
1343        _mm256_storeu_ps(idxs.as_mut_ptr(), acc_idx);
1344        let (mut min_idx, mut min_val) = reduce_lane_argmin_f32(&vals, &idxs);
1345
1346        while i < len {
1347            let diff = a[i] - b[i];
1348            if diff < min_val {
1349                min_val = diff;
1350                min_idx = i;
1351            }
1352            i += 1;
1353        }
1354
1355        (min_idx, min_val)
1356    }
1357}
1358
1359/// Fused subtract-and-argmin over two slices of f32 (512-bit)
1360///
1361/// Uses mask registers rather than a blend vector, so the compare produces a
1362/// `__mmask16` directly and the tie-break folds with plain integer bit ops.
1363///
1364/// ### Params
1365///
1366/// * `a` - The minuend slice.
1367/// * `b` - The subtrahend slice, same length as `a`.
1368///
1369/// ### Returns
1370///
1371/// `(index, value)` of the smallest `a[i] - b[i]`, ties to the lowest index.
1372#[cfg(target_arch = "x86_64")]
1373#[target_feature(enable = "avx512f")]
1374unsafe fn argmin_diff_avx512_f32(a: &[f32], b: &[f32]) -> (usize, f32) {
1375    const W: usize = 16;
1376    const BLOCK: usize = W * UNROLL;
1377
1378    unsafe {
1379        let len = a.len();
1380        let a_ptr = a.as_ptr();
1381        let b_ptr = b.as_ptr();
1382        let n_blocks = len / BLOCK;
1383
1384        let mut best = [_mm512_set1_ps(f32::INFINITY); UNROLL];
1385        let mut best_idx = [_mm512_setzero_ps(); UNROLL];
1386        let lane_seed: [f32; W] = std::array::from_fn(|i| i as f32);
1387        let lane_base = _mm512_loadu_ps(lane_seed.as_ptr());
1388        let mut lane: [__m512; UNROLL] =
1389            std::array::from_fn(|u| _mm512_add_ps(lane_base, _mm512_set1_ps((u * W) as f32)));
1390        let step = _mm512_set1_ps(BLOCK as f32);
1391
1392        for i in 0..n_blocks {
1393            let base = i * BLOCK;
1394            for u in 0..UNROLL {
1395                let off = base + u * W;
1396                let va = _mm512_loadu_ps(a_ptr.add(off));
1397                let vb = _mm512_loadu_ps(b_ptr.add(off));
1398                let diff = _mm512_sub_ps(va, vb);
1399                // Strict `<` keeps the earliest block within a lane; ties
1400                // between accumulators are resolved in the fold below.
1401                let mask = _mm512_cmp_ps_mask(diff, best[u], _CMP_LT_OQ);
1402                best[u] = _mm512_mask_blend_ps(mask, best[u], diff);
1403                best_idx[u] = _mm512_mask_blend_ps(mask, best_idx[u], lane[u]);
1404                lane[u] = _mm512_add_ps(lane[u], step);
1405            }
1406        }
1407
1408        let mut acc = best[0];
1409        let mut acc_idx = best_idx[0];
1410        for u in 1..UNROLL {
1411            let lt = _mm512_cmp_ps_mask(best[u], acc, _CMP_LT_OQ);
1412            let eq = _mm512_cmp_ps_mask(best[u], acc, _CMP_EQ_OQ);
1413            let ilt = _mm512_cmp_ps_mask(best_idx[u], acc_idx, _CMP_LT_OQ);
1414            let take = lt | (eq & ilt);
1415            acc = _mm512_mask_blend_ps(take, acc, best[u]);
1416            acc_idx = _mm512_mask_blend_ps(take, acc_idx, best_idx[u]);
1417        }
1418
1419        // See the SSE arm: leftover whole vectors carry strictly higher
1420        // indices, so the strict `<` is still the right tie-break.
1421        let mut i = n_blocks * BLOCK;
1422        while i + W <= len {
1423            let diff = _mm512_sub_ps(_mm512_loadu_ps(a_ptr.add(i)), _mm512_loadu_ps(b_ptr.add(i)));
1424            let lane_v = _mm512_add_ps(lane_base, _mm512_set1_ps(i as f32));
1425            let mask = _mm512_cmp_ps_mask(diff, acc, _CMP_LT_OQ);
1426            acc = _mm512_mask_blend_ps(mask, acc, diff);
1427            acc_idx = _mm512_mask_blend_ps(mask, acc_idx, lane_v);
1428            i += W;
1429        }
1430
1431        let mut vals = [0.0f32; W];
1432        let mut idxs = [0.0f32; W];
1433        _mm512_storeu_ps(vals.as_mut_ptr(), acc);
1434        _mm512_storeu_ps(idxs.as_mut_ptr(), acc_idx);
1435        let (mut min_idx, mut min_val) = reduce_lane_argmin_f32(&vals, &idxs);
1436
1437        while i < len {
1438            let diff = a[i] - b[i];
1439            if diff < min_val {
1440                min_val = diff;
1441                min_idx = i;
1442            }
1443            i += 1;
1444        }
1445
1446        (min_idx, min_val)
1447    }
1448}
1449
1450/// Fused subtract-and-argmin over two slices of f32 (dispatch)
1451///
1452/// Computes `argmin_i (a[i] - b[i])` without materialising the difference. The
1453/// scalar form of this loop does not auto-vectorise, because the running
1454/// minimum is a data-dependent branch, so it is the one pass in the Frank-Wolfe
1455/// column update that stays scalar unless it is written out by hand.
1456///
1457/// The compare-and-blend chain is loop-carried, so each vector arm carries
1458/// `UNROLL` independent accumulator pairs, folds them once the block loop is
1459/// done, sweeps any leftover whole vectors through the merged pair, and only
1460/// then falls back to scalar for the last `< W` elements. Leaving the whole
1461/// `BLOCK` remainder to the scalar tail costs more than the unroll wins at
1462/// small `k`, which is why the extra pass is there.
1463///
1464/// Two caveats, neither reachable from the Frank-Wolfe callers but both visible
1465/// to anyone else calling this:
1466///
1467/// - Lane indices are carried as `f32`, exact only below `2^24`. Longer slices
1468///   would return a rounded index.
1469/// - Non-finite differences are not handled consistently across the arms. The
1470///   scalar path seeds from `a[0] - b[0]`, so a `NaN` there poisons every later
1471///   comparison and it returns index 0; the vector paths seed at `+INFINITY`
1472///   and never blend a `NaN` in, so they return the minimum over the finite
1473///   differences. A lane whose only candidates are `+INFINITY` keeps its seed
1474///   index. Filter non-finite values before calling if they are possible in
1475///   your data.
1476///
1477/// ### Params
1478///
1479/// * `a` - The minuend slice. Must be non-empty and shorter than `2^24`.
1480/// * `b` - The subtrahend slice, same length as `a`.
1481///
1482/// ### Returns
1483///
1484/// `(index, value)` of the smallest `a[i] - b[i]`, ties to the lowest index.
1485#[inline]
1486pub fn argmin_diff_simd_f32(a: &[f32], b: &[f32]) -> (usize, f32) {
1487    debug_assert_eq!(a.len(), b.len(), "slices must match in length");
1488    debug_assert!(!a.is_empty(), "argmin over an empty slice is undefined");
1489    debug_assert!(
1490        a.len() < (1usize << 24),
1491        "lane indices are carried as f32 and stop being exact at 2^24"
1492    );
1493
1494    #[cfg(target_arch = "x86_64")]
1495    // SAFETY: see `sum_squares_simd_f32`.
1496    unsafe {
1497        match detect_simd_level() {
1498            SimdLevel::Avx512 => argmin_diff_avx512_f32(a, b),
1499            SimdLevel::Avx2 => argmin_diff_avx2_f32(a, b),
1500            SimdLevel::Sse => argmin_diff_sse_f32(a, b),
1501            SimdLevel::Scalar => argmin_diff_scalar_f32(a, b),
1502        }
1503    }
1504
1505    #[cfg(target_arch = "aarch64")]
1506    // SAFETY: NEON is baseline on aarch64, so the feature is unconditionally
1507    // present and `detect_simd_level` always reports `Sse` here.
1508    unsafe {
1509        match detect_simd_level() {
1510            SimdLevel::Sse => argmin_diff_neon_f32(a, b),
1511            _ => argmin_diff_scalar_f32(a, b),
1512        }
1513    }
1514
1515    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1516    argmin_diff_scalar_f32(a, b)
1517}
1518
1519//////////
1520// Sums //
1521//////////
1522
1523/////////
1524// f32 //
1525/////////
1526
1527/// SIMD sum of a slice of f32 (scalar)
1528///
1529/// ### Params
1530///
1531/// * `a` - The slice of f32 values to sum.
1532///
1533/// ### Returns
1534///
1535/// Sum
1536#[inline(always)]
1537fn sum_scalar_f32(a: &[f32]) -> f32 {
1538    a.iter().sum()
1539}
1540
1541/// SIMD sum of a slice of f32 (128-bit)
1542///
1543/// ### Params
1544///
1545/// * `a` - The slice of f32 values to sum.
1546///
1547/// ### Returns
1548///
1549/// Sum
1550#[inline(always)]
1551fn sum_sse_f32(a: &[f32]) -> f32 {
1552    const W: usize = 4;
1553    const BLOCK: usize = W * UNROLL;
1554
1555    let len = a.len();
1556    let mut acc = [f32x4::ZERO; UNROLL];
1557
1558    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
1559    // or below `len - W`.
1560    unsafe {
1561        let a_ptr = a.as_ptr();
1562
1563        let n_blocks = len / BLOCK;
1564        for i in 0..n_blocks {
1565            let base = i * BLOCK;
1566            for (u, acc) in acc.iter_mut().enumerate() {
1567                let off = base + u * W;
1568                *acc += f32x4::from(*(a_ptr.add(off) as *const [f32; W]));
1569            }
1570        }
1571
1572        let mut total = acc[0];
1573        for acc in &acc[1..] {
1574            total += *acc;
1575        }
1576
1577        let mut i = n_blocks * BLOCK;
1578        while i + W <= len {
1579            total += f32x4::from(*(a_ptr.add(i) as *const [f32; W]));
1580            i += W;
1581        }
1582
1583        let mut sum = total.reduce_add();
1584        while i < len {
1585            sum += a[i];
1586            i += 1;
1587        }
1588        sum
1589    }
1590}
1591
1592/// SIMD sum of a slice of f32 (256-bit)
1593///
1594/// ### Params
1595///
1596/// * `a` - The slice of f32 values to sum.
1597///
1598/// ### Returns
1599///
1600/// Sum
1601#[cfg(target_arch = "x86_64")]
1602#[target_feature(enable = "avx2", enable = "fma")]
1603unsafe fn sum_avx2_f32(a: &[f32]) -> f32 {
1604    const W: usize = 8;
1605    const BLOCK: usize = W * UNROLL;
1606
1607    unsafe {
1608        let len = a.len();
1609        let a_ptr = a.as_ptr();
1610        let mut acc = [_mm256_setzero_ps(); UNROLL];
1611
1612        let n_blocks = len / BLOCK;
1613        for i in 0..n_blocks {
1614            let base = i * BLOCK;
1615            for (u, acc) in acc.iter_mut().enumerate() {
1616                *acc = _mm256_add_ps(*acc, _mm256_loadu_ps(a_ptr.add(base + u * W)));
1617            }
1618        }
1619
1620        let mut total = acc[0];
1621        for acc in &acc[1..] {
1622            total = _mm256_add_ps(total, *acc);
1623        }
1624
1625        let mut i = n_blocks * BLOCK;
1626        while i + W <= len {
1627            total = _mm256_add_ps(total, _mm256_loadu_ps(a_ptr.add(i)));
1628            i += W;
1629        }
1630
1631        let mut sum = hsum_avx_f32(total);
1632        while i < len {
1633            sum += a[i];
1634            i += 1;
1635        }
1636        sum
1637    }
1638}
1639
1640/// SIMD sum of a slice of f32 (512-bit)
1641///
1642/// ### Params
1643///
1644/// * `a` - The slice of f32 values to sum.
1645///
1646/// ### Returns
1647///
1648/// Sum
1649#[cfg(target_arch = "x86_64")]
1650#[target_feature(enable = "avx512f")]
1651unsafe fn sum_avx512_f32(a: &[f32]) -> f32 {
1652    const W: usize = 16;
1653    const BLOCK: usize = W * UNROLL;
1654
1655    unsafe {
1656        let len = a.len();
1657        let a_ptr = a.as_ptr();
1658        let mut acc = [_mm512_setzero_ps(); UNROLL];
1659
1660        let n_blocks = len / BLOCK;
1661        for i in 0..n_blocks {
1662            let base = i * BLOCK;
1663            for (u, acc) in acc.iter_mut().enumerate() {
1664                *acc = _mm512_add_ps(*acc, _mm512_loadu_ps(a_ptr.add(base + u * W)));
1665            }
1666        }
1667
1668        let mut total = acc[0];
1669        for acc in &acc[1..] {
1670            total = _mm512_add_ps(total, *acc);
1671        }
1672
1673        let mut i = n_blocks * BLOCK;
1674        while i + W <= len {
1675            total = _mm512_add_ps(total, _mm512_loadu_ps(a_ptr.add(i)));
1676            i += W;
1677        }
1678
1679        let mut sum = _mm512_reduce_add_ps(total);
1680        while i < len {
1681            sum += a[i];
1682            i += 1;
1683        }
1684        sum
1685    }
1686}
1687
1688/// SIMD sum of a slice of f32 (dispatch)
1689///
1690/// ### Params
1691///
1692/// * `a` - The slice of f32 values to sum.
1693///
1694/// ### Returns
1695///
1696/// Sum
1697#[inline]
1698pub fn sum_simd_f32(a: &[f32]) -> f32 {
1699    #[cfg(target_arch = "x86_64")]
1700    // SAFETY: see `sum_squares_simd_f32`.
1701    unsafe {
1702        match detect_simd_level() {
1703            SimdLevel::Avx512 => sum_avx512_f32(a),
1704            SimdLevel::Avx2 => sum_avx2_f32(a),
1705            SimdLevel::Sse => sum_sse_f32(a),
1706            SimdLevel::Scalar => sum_scalar_f32(a),
1707        }
1708    }
1709
1710    #[cfg(not(target_arch = "x86_64"))]
1711    match detect_simd_level() {
1712        SimdLevel::Sse => sum_sse_f32(a),
1713        _ => sum_scalar_f32(a),
1714    }
1715}
1716
1717/////////
1718// f64 //
1719/////////
1720
1721/// SIMD sum of a slice of f64 (scalar)
1722///
1723/// ### Params
1724///
1725/// * `a` - The slice of f64 values to sum.
1726///
1727/// ### Returns
1728///
1729/// Sum
1730#[inline(always)]
1731fn sum_scalar_f64(a: &[f64]) -> f64 {
1732    a.iter().sum()
1733}
1734
1735/// SIMD sum of a slice of f64 (128-bit)
1736///
1737/// ### Params
1738///
1739/// * `a` - The slice of f64 values to sum.
1740///
1741/// ### Returns
1742///
1743/// Sum
1744#[inline(always)]
1745fn sum_sse_f64(a: &[f64]) -> f64 {
1746    const W: usize = 2;
1747    const BLOCK: usize = W * UNROLL;
1748
1749    let len = a.len();
1750    let mut acc = [f64x2::ZERO; UNROLL];
1751
1752    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
1753    // or below `len - W`.
1754    unsafe {
1755        let a_ptr = a.as_ptr();
1756
1757        let n_blocks = len / BLOCK;
1758        for i in 0..n_blocks {
1759            let base = i * BLOCK;
1760            for (u, acc) in acc.iter_mut().enumerate() {
1761                let off = base + u * W;
1762                *acc += f64x2::from(*(a_ptr.add(off) as *const [f64; W]));
1763            }
1764        }
1765
1766        let mut total = acc[0];
1767        for acc in &acc[1..] {
1768            total += *acc;
1769        }
1770
1771        let mut i = n_blocks * BLOCK;
1772        while i + W <= len {
1773            total += f64x2::from(*(a_ptr.add(i) as *const [f64; W]));
1774            i += W;
1775        }
1776
1777        let mut sum = total.reduce_add();
1778        while i < len {
1779            sum += a[i];
1780            i += 1;
1781        }
1782        sum
1783    }
1784}
1785
1786/// SIMD sum of a slice of f64 (256-bit)
1787///
1788/// ### Params
1789///
1790/// * `a` - The slice of f64 values to sum.
1791///
1792/// ### Returns
1793///
1794/// Sum
1795#[cfg(target_arch = "x86_64")]
1796#[target_feature(enable = "avx2", enable = "fma")]
1797unsafe fn sum_avx2_f64(a: &[f64]) -> f64 {
1798    const W: usize = 4;
1799    const BLOCK: usize = W * UNROLL;
1800
1801    unsafe {
1802        let len = a.len();
1803        let a_ptr = a.as_ptr();
1804        let mut acc = [_mm256_setzero_pd(); UNROLL];
1805
1806        let n_blocks = len / BLOCK;
1807        for i in 0..n_blocks {
1808            let base = i * BLOCK;
1809            for (u, acc) in acc.iter_mut().enumerate() {
1810                *acc = _mm256_add_pd(*acc, _mm256_loadu_pd(a_ptr.add(base + u * W)));
1811            }
1812        }
1813
1814        let mut total = acc[0];
1815        for acc in &acc[1..] {
1816            total = _mm256_add_pd(total, *acc);
1817        }
1818
1819        let mut i = n_blocks * BLOCK;
1820        while i + W <= len {
1821            total = _mm256_add_pd(total, _mm256_loadu_pd(a_ptr.add(i)));
1822            i += W;
1823        }
1824
1825        let mut sum = hsum_avx_f64(total);
1826        while i < len {
1827            sum += a[i];
1828            i += 1;
1829        }
1830        sum
1831    }
1832}
1833
1834/// SIMD sum of a slice of f64 (512-bit)
1835///
1836/// ### Params
1837///
1838/// * `a` - The slice of f64 values to sum.
1839///
1840/// ### Returns
1841///
1842/// Sum
1843#[cfg(target_arch = "x86_64")]
1844#[target_feature(enable = "avx512f")]
1845unsafe fn sum_avx512_f64(a: &[f64]) -> f64 {
1846    const W: usize = 8;
1847    const BLOCK: usize = W * UNROLL;
1848
1849    unsafe {
1850        let len = a.len();
1851        let a_ptr = a.as_ptr();
1852        let mut acc = [_mm512_setzero_pd(); UNROLL];
1853
1854        let n_blocks = len / BLOCK;
1855        for i in 0..n_blocks {
1856            let base = i * BLOCK;
1857            for (u, acc) in acc.iter_mut().enumerate() {
1858                *acc = _mm512_add_pd(*acc, _mm512_loadu_pd(a_ptr.add(base + u * W)));
1859            }
1860        }
1861
1862        let mut total = acc[0];
1863        for acc in &acc[1..] {
1864            total = _mm512_add_pd(total, *acc);
1865        }
1866
1867        let mut i = n_blocks * BLOCK;
1868        while i + W <= len {
1869            total = _mm512_add_pd(total, _mm512_loadu_pd(a_ptr.add(i)));
1870            i += W;
1871        }
1872
1873        let mut sum = _mm512_reduce_add_pd(total);
1874        while i < len {
1875            sum += a[i];
1876            i += 1;
1877        }
1878        sum
1879    }
1880}
1881
1882/// SIMD sum of a slice of f64 (dispatch)
1883///
1884/// ### Params
1885///
1886/// * `a` - The slice of f64 values to sum.
1887///
1888/// ### Returns
1889///
1890/// Sum
1891#[inline]
1892pub fn sum_simd_f64(a: &[f64]) -> f64 {
1893    #[cfg(target_arch = "x86_64")]
1894    // SAFETY: see `sum_squares_simd_f32`.
1895    unsafe {
1896        match detect_simd_level() {
1897            SimdLevel::Avx512 => sum_avx512_f64(a),
1898            SimdLevel::Avx2 => sum_avx2_f64(a),
1899            SimdLevel::Sse => sum_sse_f64(a),
1900            SimdLevel::Scalar => sum_scalar_f64(a),
1901        }
1902    }
1903
1904    #[cfg(not(target_arch = "x86_64"))]
1905    match detect_simd_level() {
1906        SimdLevel::Sse => sum_sse_f64(a),
1907        _ => sum_scalar_f64(a),
1908    }
1909}
1910
1911//////////////
1912// Variance //
1913//////////////
1914
1915/////////
1916// f32 //
1917/////////
1918
1919/// SIMD sum of squared deviations of a slice of f32 (scalar)
1920///
1921/// ### Params
1922///
1923/// * `a` - The slice of f32 values to calculate variance for.
1924/// * `mean` - The mean of the values in `a`.
1925///
1926/// ### Returns
1927///
1928/// Sum of squared deviations from the mean
1929#[inline(always)]
1930fn sum_squared_dev_scalar_f32(a: &[f32], mean: f32) -> f32 {
1931    a.iter().map(|&x| (x - mean).powi(2)).sum::<f32>()
1932}
1933
1934/// SIMD sum of squared deviations of a slice of f32 (128-bit)
1935///
1936/// ### Params
1937///
1938/// * `a` - The slice of f32 values to calculate variance for.
1939/// * `mean` - The mean of the values in `a`.
1940///
1941/// ### Returns
1942///
1943/// Sum of squared deviations from the mean
1944#[inline(always)]
1945fn sum_squared_dev_sse_f32(a: &[f32], mean: f32) -> f32 {
1946    const W: usize = 4;
1947    const BLOCK: usize = W * UNROLL;
1948
1949    let len = a.len();
1950    let mut acc = [f32x4::ZERO; UNROLL];
1951    let mean_vec = f32x4::splat(mean);
1952
1953    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
1954    // or below `len - W`.
1955    unsafe {
1956        let a_ptr = a.as_ptr();
1957
1958        let n_blocks = len / BLOCK;
1959        for i in 0..n_blocks {
1960            let base = i * BLOCK;
1961            for (u, acc) in acc.iter_mut().enumerate() {
1962                let off = base + u * W;
1963                let diff = f32x4::from(*(a_ptr.add(off) as *const [f32; W])) - mean_vec;
1964                *acc += diff * diff;
1965            }
1966        }
1967
1968        let mut total = acc[0];
1969        for acc in &acc[1..] {
1970            total += *acc;
1971        }
1972
1973        let mut i = n_blocks * BLOCK;
1974        while i + W <= len {
1975            let diff = f32x4::from(*(a_ptr.add(i) as *const [f32; W])) - mean_vec;
1976            total += diff * diff;
1977            i += W;
1978        }
1979
1980        let mut sum = total.reduce_add();
1981        while i < len {
1982            let diff = a[i] - mean;
1983            sum += diff * diff;
1984            i += 1;
1985        }
1986        sum
1987    }
1988}
1989
1990/// SIMD sum of squared deviations of a slice of f32 (256-bit)
1991///
1992/// ### Params
1993///
1994/// * `a` - The slice of f32 values to calculate variance for.
1995/// * `mean` - The mean of the values in `a`.
1996///
1997/// ### Returns
1998///
1999/// Sum of squared deviations from the mean
2000#[cfg(target_arch = "x86_64")]
2001#[target_feature(enable = "avx2", enable = "fma")]
2002unsafe fn sum_squared_dev_avx2_f32(a: &[f32], mean: f32) -> f32 {
2003    const W: usize = 8;
2004    const BLOCK: usize = W * UNROLL;
2005
2006    unsafe {
2007        let len = a.len();
2008        let a_ptr = a.as_ptr();
2009        let mean_vec = _mm256_set1_ps(mean);
2010        let mut acc = [_mm256_setzero_ps(); UNROLL];
2011
2012        let n_blocks = len / BLOCK;
2013        for i in 0..n_blocks {
2014            let base = i * BLOCK;
2015            for (u, acc) in acc.iter_mut().enumerate() {
2016                let diff = _mm256_sub_ps(_mm256_loadu_ps(a_ptr.add(base + u * W)), mean_vec);
2017                *acc = _mm256_fmadd_ps(diff, diff, *acc);
2018            }
2019        }
2020
2021        let mut total = acc[0];
2022        for acc in &acc[1..] {
2023            total = _mm256_add_ps(total, *acc);
2024        }
2025
2026        let mut i = n_blocks * BLOCK;
2027        while i + W <= len {
2028            let diff = _mm256_sub_ps(_mm256_loadu_ps(a_ptr.add(i)), mean_vec);
2029            total = _mm256_fmadd_ps(diff, diff, total);
2030            i += W;
2031        }
2032
2033        let mut sum = hsum_avx_f32(total);
2034        while i < len {
2035            let diff = a[i] - mean;
2036            sum += diff * diff;
2037            i += 1;
2038        }
2039        sum
2040    }
2041}
2042
2043/// SIMD sum of squared deviations of a slice of f32 (512-bit)
2044///
2045/// ### Params
2046///
2047/// * `a` - The slice of f32 values to calculate variance for.
2048/// * `mean` - The mean of the values in `a`.
2049///
2050/// ### Returns
2051///
2052/// Sum of squared deviations from the mean
2053#[cfg(target_arch = "x86_64")]
2054#[target_feature(enable = "avx512f")]
2055unsafe fn sum_squared_dev_avx512_f32(a: &[f32], mean: f32) -> f32 {
2056    const W: usize = 16;
2057    const BLOCK: usize = W * UNROLL;
2058
2059    unsafe {
2060        let len = a.len();
2061        let a_ptr = a.as_ptr();
2062        let mean_vec = _mm512_set1_ps(mean);
2063        let mut acc = [_mm512_setzero_ps(); UNROLL];
2064
2065        let n_blocks = len / BLOCK;
2066        for i in 0..n_blocks {
2067            let base = i * BLOCK;
2068            for (u, acc) in acc.iter_mut().enumerate() {
2069                let diff = _mm512_sub_ps(_mm512_loadu_ps(a_ptr.add(base + u * W)), mean_vec);
2070                *acc = _mm512_fmadd_ps(diff, diff, *acc);
2071            }
2072        }
2073
2074        let mut total = acc[0];
2075        for acc in &acc[1..] {
2076            total = _mm512_add_ps(total, *acc);
2077        }
2078
2079        let mut i = n_blocks * BLOCK;
2080        while i + W <= len {
2081            let diff = _mm512_sub_ps(_mm512_loadu_ps(a_ptr.add(i)), mean_vec);
2082            total = _mm512_fmadd_ps(diff, diff, total);
2083            i += W;
2084        }
2085
2086        let mut sum = _mm512_reduce_add_ps(total);
2087        while i < len {
2088            let diff = a[i] - mean;
2089            sum += diff * diff;
2090            i += 1;
2091        }
2092        sum
2093    }
2094}
2095
2096/// SIMD sum of squared deviations of a slice of f32 (dispatch)
2097///
2098/// ### Params
2099///
2100/// * `a` - The slice of f32 values to calculate variance for.
2101/// * `mean` - The mean of the values in `a`.
2102///
2103/// ### Returns
2104///
2105/// Sum of squared deviations from the mean
2106#[inline]
2107pub fn sum_squared_dev_simd_f32(a: &[f32], mean: f32) -> f32 {
2108    #[cfg(target_arch = "x86_64")]
2109    // SAFETY: see `sum_squares_simd_f32`.
2110    unsafe {
2111        match detect_simd_level() {
2112            SimdLevel::Avx512 => sum_squared_dev_avx512_f32(a, mean),
2113            SimdLevel::Avx2 => sum_squared_dev_avx2_f32(a, mean),
2114            SimdLevel::Sse => sum_squared_dev_sse_f32(a, mean),
2115            SimdLevel::Scalar => sum_squared_dev_scalar_f32(a, mean),
2116        }
2117    }
2118
2119    #[cfg(not(target_arch = "x86_64"))]
2120    match detect_simd_level() {
2121        SimdLevel::Sse => sum_squared_dev_sse_f32(a, mean),
2122        _ => sum_squared_dev_scalar_f32(a, mean),
2123    }
2124}
2125
2126/////////
2127// f64 //
2128/////////
2129
2130/// SIMD sum of squared deviations of a slice of f64 (scalar)
2131///
2132/// ### Params
2133///
2134/// * `a` - The slice of f64 values to calculate variance for.
2135/// * `mean` - The mean of the values in `a`.
2136///
2137/// ### Returns
2138///
2139/// Sum of squared deviations from the mean
2140#[inline(always)]
2141fn sum_squared_dev_scalar_f64(a: &[f64], mean: f64) -> f64 {
2142    a.iter().map(|&x| (x - mean).powi(2)).sum::<f64>()
2143}
2144
2145/// SIMD sum of squared deviations of a slice of f64 (128-bit)
2146///
2147/// ### Params
2148///
2149/// * `a` - The slice of f64 values to calculate variance for.
2150/// * `mean` - The mean of the values in `a`.
2151///
2152/// ### Returns
2153///
2154/// Sum of squared deviations from the mean
2155#[inline(always)]
2156fn sum_squared_dev_sse_f64(a: &[f64], mean: f64) -> f64 {
2157    const W: usize = 2;
2158    const BLOCK: usize = W * UNROLL;
2159
2160    let len = a.len();
2161    let mut acc = [f64x2::ZERO; UNROLL];
2162    let mean_vec = f64x2::splat(mean);
2163
2164    // SAFETY: every load reads W lanes from an offset the loop bounds keep at
2165    // or below `len - W`.
2166    unsafe {
2167        let a_ptr = a.as_ptr();
2168
2169        let n_blocks = len / BLOCK;
2170        for i in 0..n_blocks {
2171            let base = i * BLOCK;
2172            for (u, acc) in acc.iter_mut().enumerate() {
2173                let off = base + u * W;
2174                let diff = f64x2::from(*(a_ptr.add(off) as *const [f64; W])) - mean_vec;
2175                *acc += diff * diff;
2176            }
2177        }
2178
2179        let mut total = acc[0];
2180        for acc in &acc[1..] {
2181            total += *acc;
2182        }
2183
2184        let mut i = n_blocks * BLOCK;
2185        while i + W <= len {
2186            let diff = f64x2::from(*(a_ptr.add(i) as *const [f64; W])) - mean_vec;
2187            total += diff * diff;
2188            i += W;
2189        }
2190
2191        let mut sum = total.reduce_add();
2192        while i < len {
2193            let diff = a[i] - mean;
2194            sum += diff * diff;
2195            i += 1;
2196        }
2197        sum
2198    }
2199}
2200
2201/// SIMD sum of squared deviations of a slice of f64 (256-bit)
2202///
2203/// ### Params
2204///
2205/// * `a` - The slice of f64 values to calculate variance for.
2206/// * `mean` - The mean of the values in `a`.
2207///
2208/// ### Returns
2209///
2210/// Sum of squared deviations from the mean
2211#[cfg(target_arch = "x86_64")]
2212#[target_feature(enable = "avx2", enable = "fma")]
2213unsafe fn sum_squared_dev_avx2_f64(a: &[f64], mean: f64) -> f64 {
2214    const W: usize = 4;
2215    const BLOCK: usize = W * UNROLL;
2216
2217    unsafe {
2218        let len = a.len();
2219        let a_ptr = a.as_ptr();
2220        let mean_vec = _mm256_set1_pd(mean);
2221        let mut acc = [_mm256_setzero_pd(); UNROLL];
2222
2223        let n_blocks = len / BLOCK;
2224        for i in 0..n_blocks {
2225            let base = i * BLOCK;
2226            for (u, acc) in acc.iter_mut().enumerate() {
2227                let diff = _mm256_sub_pd(_mm256_loadu_pd(a_ptr.add(base + u * W)), mean_vec);
2228                *acc = _mm256_fmadd_pd(diff, diff, *acc);
2229            }
2230        }
2231
2232        let mut total = acc[0];
2233        for acc in &acc[1..] {
2234            total = _mm256_add_pd(total, *acc);
2235        }
2236
2237        let mut i = n_blocks * BLOCK;
2238        while i + W <= len {
2239            let diff = _mm256_sub_pd(_mm256_loadu_pd(a_ptr.add(i)), mean_vec);
2240            total = _mm256_fmadd_pd(diff, diff, total);
2241            i += W;
2242        }
2243
2244        let mut sum = hsum_avx_f64(total);
2245        while i < len {
2246            let diff = a[i] - mean;
2247            sum += diff * diff;
2248            i += 1;
2249        }
2250        sum
2251    }
2252}
2253
2254/// SIMD sum of squared deviations of a slice of f64 (512-bit)
2255///
2256/// ### Params
2257///
2258/// * `a` - The slice of f64 values to calculate variance for.
2259/// * `mean` - The mean of the values in `a`.
2260///
2261/// ### Returns
2262///
2263/// Sum of squared deviations from the mean
2264#[cfg(target_arch = "x86_64")]
2265#[target_feature(enable = "avx512f")]
2266unsafe fn sum_squared_dev_avx512_f64(a: &[f64], mean: f64) -> f64 {
2267    const W: usize = 8;
2268    const BLOCK: usize = W * UNROLL;
2269
2270    unsafe {
2271        let len = a.len();
2272        let a_ptr = a.as_ptr();
2273        let mean_vec = _mm512_set1_pd(mean);
2274        let mut acc = [_mm512_setzero_pd(); UNROLL];
2275
2276        let n_blocks = len / BLOCK;
2277        for i in 0..n_blocks {
2278            let base = i * BLOCK;
2279            for (u, acc) in acc.iter_mut().enumerate() {
2280                let diff = _mm512_sub_pd(_mm512_loadu_pd(a_ptr.add(base + u * W)), mean_vec);
2281                *acc = _mm512_fmadd_pd(diff, diff, *acc);
2282            }
2283        }
2284
2285        let mut total = acc[0];
2286        for acc in &acc[1..] {
2287            total = _mm512_add_pd(total, *acc);
2288        }
2289
2290        let mut i = n_blocks * BLOCK;
2291        while i + W <= len {
2292            let diff = _mm512_sub_pd(_mm512_loadu_pd(a_ptr.add(i)), mean_vec);
2293            total = _mm512_fmadd_pd(diff, diff, total);
2294            i += W;
2295        }
2296
2297        let mut sum = _mm512_reduce_add_pd(total);
2298        while i < len {
2299            let diff = a[i] - mean;
2300            sum += diff * diff;
2301            i += 1;
2302        }
2303        sum
2304    }
2305}
2306
2307/// SIMD sum of squared deviations of a slice of f64 (dispatch)
2308///
2309/// ### Params
2310///
2311/// * `a` - The slice of f64 values to calculate variance for.
2312/// * `mean` - The mean of the values in `a`.
2313///
2314/// ### Returns
2315///
2316/// Sum of squared deviations from the mean
2317#[inline]
2318pub fn sum_squared_dev_simd_f64(a: &[f64], mean: f64) -> f64 {
2319    #[cfg(target_arch = "x86_64")]
2320    // SAFETY: see `sum_squares_simd_f32`.
2321    unsafe {
2322        match detect_simd_level() {
2323            SimdLevel::Avx512 => sum_squared_dev_avx512_f64(a, mean),
2324            SimdLevel::Avx2 => sum_squared_dev_avx2_f64(a, mean),
2325            SimdLevel::Sse => sum_squared_dev_sse_f64(a, mean),
2326            SimdLevel::Scalar => sum_squared_dev_scalar_f64(a, mean),
2327        }
2328    }
2329
2330    #[cfg(not(target_arch = "x86_64"))]
2331    match detect_simd_level() {
2332        SimdLevel::Sse => sum_squared_dev_sse_f64(a, mean),
2333        _ => sum_squared_dev_scalar_f64(a, mean),
2334    }
2335}
2336
2337//////////
2338// Axpy //
2339//////////
2340
2341// `y += a * x`. Unlike every reduction above this is a store loop, so there is
2342// no accumulator dependency chain to hide and `UNROLL` buys nothing: the kernel
2343// is bound by the two streams it moves, not by FMA latency. Each arm is a plain
2344// vector loop with a scalar tail.
2345
2346/////////
2347// f32 //
2348/////////
2349
2350/// Scaled vector addition `y += a * x` for f32 (scalar)
2351///
2352/// ### Params
2353///
2354/// * `y` - Accumulator, updated in place.
2355/// * `a` - Scalar multiplier.
2356/// * `x` - Slice added into `y` (must be the same length as `y`).
2357#[inline(always)]
2358fn axpy_scalar_f32(y: &mut [f32], a: f32, x: &[f32]) {
2359    for (y_i, &x_i) in y.iter_mut().zip(x.iter()) {
2360        *y_i += a * x_i;
2361    }
2362}
2363
2364/// Scaled vector addition `y += a * x` for f32 (128-bit)
2365///
2366/// ### Params
2367///
2368/// * `y` - Accumulator, updated in place.
2369/// * `a` - Scalar multiplier.
2370/// * `x` - Slice added into `y` (must be the same length as `y`).
2371#[inline(always)]
2372fn axpy_sse_f32(y: &mut [f32], a: f32, x: &[f32]) {
2373    const W: usize = 4;
2374    let len = y.len();
2375    let chunks = len / W;
2376    let va = f32x4::splat(a);
2377
2378    unsafe {
2379        let x_ptr = x.as_ptr();
2380        let y_ptr = y.as_mut_ptr();
2381        for i in 0..chunks {
2382            let off = i * W;
2383            let vx = f32x4::from(*(x_ptr.add(off) as *const [f32; W]));
2384            let vy = f32x4::from(*(y_ptr.add(off) as *const [f32; W]));
2385            *(y_ptr.add(off) as *mut [f32; W]) = vx.mul_add(va, vy).to_array();
2386        }
2387    }
2388
2389    axpy_scalar_f32(&mut y[chunks * W..], a, &x[chunks * W..]);
2390}
2391
2392/// Scaled vector addition `y += a * x` for f32 (256-bit)
2393///
2394/// ### Params
2395///
2396/// * `y` - Accumulator, updated in place.
2397/// * `a` - Scalar multiplier.
2398/// * `x` - Slice added into `y` (must be the same length as `y`).
2399#[cfg(target_arch = "x86_64")]
2400#[target_feature(enable = "avx2", enable = "fma")]
2401unsafe fn axpy_avx2_f32(y: &mut [f32], a: f32, x: &[f32]) {
2402    const W: usize = 8;
2403    let len = y.len();
2404    let chunks = len / W;
2405
2406    unsafe {
2407        let va = _mm256_set1_ps(a);
2408        let x_ptr = x.as_ptr();
2409        let y_ptr = y.as_mut_ptr();
2410        for i in 0..chunks {
2411            let off = i * W;
2412            let vx = _mm256_loadu_ps(x_ptr.add(off));
2413            let vy = _mm256_loadu_ps(y_ptr.add(off));
2414            _mm256_storeu_ps(y_ptr.add(off), _mm256_fmadd_ps(vx, va, vy));
2415        }
2416    }
2417
2418    axpy_scalar_f32(&mut y[chunks * W..], a, &x[chunks * W..]);
2419}
2420
2421/// Scaled vector addition `y += a * x` for f32 (512-bit)
2422///
2423/// ### Params
2424///
2425/// * `y` - Accumulator, updated in place.
2426/// * `a` - Scalar multiplier.
2427/// * `x` - Slice added into `y` (must be the same length as `y`).
2428#[cfg(target_arch = "x86_64")]
2429#[target_feature(enable = "avx512f")]
2430unsafe fn axpy_avx512_f32(y: &mut [f32], a: f32, x: &[f32]) {
2431    const W: usize = 16;
2432    let len = y.len();
2433    let chunks = len / W;
2434
2435    unsafe {
2436        let va = _mm512_set1_ps(a);
2437        let x_ptr = x.as_ptr();
2438        let y_ptr = y.as_mut_ptr();
2439        for i in 0..chunks {
2440            let off = i * W;
2441            let vx = _mm512_loadu_ps(x_ptr.add(off));
2442            let vy = _mm512_loadu_ps(y_ptr.add(off));
2443            _mm512_storeu_ps(y_ptr.add(off), _mm512_fmadd_ps(vx, va, vy));
2444        }
2445    }
2446
2447    axpy_scalar_f32(&mut y[chunks * W..], a, &x[chunks * W..]);
2448}
2449
2450/// Scaled vector addition `y += a * x` for f32 (dispatch)
2451///
2452/// ### Params
2453///
2454/// * `y` - Accumulator, updated in place.
2455/// * `a` - Scalar multiplier.
2456/// * `x` - Slice added into `y`.
2457///
2458/// ### Panics
2459///
2460/// If `x` and `y` differ in length.
2461#[inline]
2462pub fn axpy_simd_f32(y: &mut [f32], a: f32, x: &[f32]) {
2463    assert_eq!(y.len(), x.len(), "axpy operands differ in length");
2464
2465    #[cfg(target_arch = "x86_64")]
2466    // SAFETY: see `sum_squares_simd_f32`.
2467    unsafe {
2468        match detect_simd_level() {
2469            SimdLevel::Avx512 => axpy_avx512_f32(y, a, x),
2470            SimdLevel::Avx2 => axpy_avx2_f32(y, a, x),
2471            SimdLevel::Sse => axpy_sse_f32(y, a, x),
2472            SimdLevel::Scalar => axpy_scalar_f32(y, a, x),
2473        }
2474    }
2475
2476    #[cfg(not(target_arch = "x86_64"))]
2477    match detect_simd_level() {
2478        SimdLevel::Sse => axpy_sse_f32(y, a, x),
2479        _ => axpy_scalar_f32(y, a, x),
2480    }
2481}
2482
2483/////////
2484// f64 //
2485/////////
2486
2487/// Scaled vector addition `y += a * x` for f64 (scalar)
2488///
2489/// ### Params
2490///
2491/// * `y` - Accumulator, updated in place.
2492/// * `a` - Scalar multiplier.
2493/// * `x` - Slice added into `y` (must be the same length as `y`).
2494#[inline(always)]
2495fn axpy_scalar_f64(y: &mut [f64], a: f64, x: &[f64]) {
2496    for (y_i, &x_i) in y.iter_mut().zip(x.iter()) {
2497        *y_i += a * x_i;
2498    }
2499}
2500
2501/// Scaled vector addition `y += a * x` for f64 (128-bit)
2502///
2503/// ### Params
2504///
2505/// * `y` - Accumulator, updated in place.
2506/// * `a` - Scalar multiplier.
2507/// * `x` - Slice added into `y` (must be the same length as `y`).
2508#[inline(always)]
2509fn axpy_sse_f64(y: &mut [f64], a: f64, x: &[f64]) {
2510    const W: usize = 2;
2511    let len = y.len();
2512    let chunks = len / W;
2513    let va = f64x2::splat(a);
2514
2515    unsafe {
2516        let x_ptr = x.as_ptr();
2517        let y_ptr = y.as_mut_ptr();
2518        for i in 0..chunks {
2519            let off = i * W;
2520            let vx = f64x2::from(*(x_ptr.add(off) as *const [f64; W]));
2521            let vy = f64x2::from(*(y_ptr.add(off) as *const [f64; W]));
2522            *(y_ptr.add(off) as *mut [f64; W]) = vx.mul_add(va, vy).to_array();
2523        }
2524    }
2525
2526    axpy_scalar_f64(&mut y[chunks * W..], a, &x[chunks * W..]);
2527}
2528
2529/// Scaled vector addition `y += a * x` for f64 (256-bit)
2530///
2531/// ### Params
2532///
2533/// * `y` - Accumulator, updated in place.
2534/// * `a` - Scalar multiplier.
2535/// * `x` - Slice added into `y` (must be the same length as `y`).
2536#[cfg(target_arch = "x86_64")]
2537#[target_feature(enable = "avx2", enable = "fma")]
2538unsafe fn axpy_avx2_f64(y: &mut [f64], a: f64, x: &[f64]) {
2539    const W: usize = 4;
2540    let len = y.len();
2541    let chunks = len / W;
2542
2543    unsafe {
2544        let va = _mm256_set1_pd(a);
2545        let x_ptr = x.as_ptr();
2546        let y_ptr = y.as_mut_ptr();
2547        for i in 0..chunks {
2548            let off = i * W;
2549            let vx = _mm256_loadu_pd(x_ptr.add(off));
2550            let vy = _mm256_loadu_pd(y_ptr.add(off));
2551            _mm256_storeu_pd(y_ptr.add(off), _mm256_fmadd_pd(vx, va, vy));
2552        }
2553    }
2554
2555    axpy_scalar_f64(&mut y[chunks * W..], a, &x[chunks * W..]);
2556}
2557
2558/// Scaled vector addition `y += a * x` for f64 (512-bit)
2559///
2560/// ### Params
2561///
2562/// * `y` - Accumulator, updated in place.
2563/// * `a` - Scalar multiplier.
2564/// * `x` - Slice added into `y` (must be the same length as `y`).
2565#[cfg(target_arch = "x86_64")]
2566#[target_feature(enable = "avx512f")]
2567unsafe fn axpy_avx512_f64(y: &mut [f64], a: f64, x: &[f64]) {
2568    const W: usize = 8;
2569    let len = y.len();
2570    let chunks = len / W;
2571
2572    unsafe {
2573        let va = _mm512_set1_pd(a);
2574        let x_ptr = x.as_ptr();
2575        let y_ptr = y.as_mut_ptr();
2576        for i in 0..chunks {
2577            let off = i * W;
2578            let vx = _mm512_loadu_pd(x_ptr.add(off));
2579            let vy = _mm512_loadu_pd(y_ptr.add(off));
2580            _mm512_storeu_pd(y_ptr.add(off), _mm512_fmadd_pd(vx, va, vy));
2581        }
2582    }
2583
2584    axpy_scalar_f64(&mut y[chunks * W..], a, &x[chunks * W..]);
2585}
2586
2587/// Scaled vector addition `y += a * x` for f64 (dispatch)
2588///
2589/// ### Params
2590///
2591/// * `y` - Accumulator, updated in place.
2592/// * `a` - Scalar multiplier.
2593/// * `x` - Slice added into `y`.
2594///
2595/// ### Panics
2596///
2597/// If `x` and `y` differ in length.
2598#[inline]
2599pub fn axpy_simd_f64(y: &mut [f64], a: f64, x: &[f64]) {
2600    assert_eq!(y.len(), x.len(), "axpy operands differ in length");
2601
2602    #[cfg(target_arch = "x86_64")]
2603    // SAFETY: see `sum_squares_simd_f32`.
2604    unsafe {
2605        match detect_simd_level() {
2606            SimdLevel::Avx512 => axpy_avx512_f64(y, a, x),
2607            SimdLevel::Avx2 => axpy_avx2_f64(y, a, x),
2608            SimdLevel::Sse => axpy_sse_f64(y, a, x),
2609            SimdLevel::Scalar => axpy_scalar_f64(y, a, x),
2610        }
2611    }
2612
2613    #[cfg(not(target_arch = "x86_64"))]
2614    match detect_simd_level() {
2615        SimdLevel::Sse => axpy_sse_f64(y, a, x),
2616        _ => axpy_scalar_f64(y, a, x),
2617    }
2618}
2619
2620////////////////////////
2621// Widening reductions //
2622////////////////////////
2623
2624// `f32` in, `f64` out. Sparse single-cell and metacell layers store counts as
2625// `f32` to halve the footprint, but a column sum over tens of thousands of
2626// entries walks straight past `2^24` and a `f32` accumulator starts dropping
2627// whole entries. Converting once on load and accumulating in `f64` costs one
2628// extra instruction per vector and removes the problem.
2629//
2630// Each arm loads its widest `f32` vector, converts both halves and folds them
2631// into one `f64` accumulator per unrolled slot.
2632
2633//////////
2634// Sums //
2635//////////
2636
2637/// Widening sum of a slice of f32, accumulated in f64 (scalar)
2638///
2639/// ### Params
2640///
2641/// * `a` - The slice of f32 values to sum.
2642///
2643/// ### Returns
2644///
2645/// Sum, in f64
2646#[inline(always)]
2647fn sum_widen_scalar_f32(a: &[f32]) -> f64 {
2648    a.iter().map(|&x| x as f64).sum()
2649}
2650
2651/// Widening sum of a slice of f32, accumulated in f64 (128-bit, SSE2)
2652///
2653/// ### Params
2654///
2655/// * `a` - The slice of f32 values to sum.
2656///
2657/// ### Returns
2658///
2659/// Sum, in f64
2660#[cfg(target_arch = "x86_64")]
2661#[target_feature(enable = "sse2")]
2662unsafe fn sum_widen_sse_f32(a: &[f32]) -> f64 {
2663    const W: usize = 4;
2664    const BLOCK: usize = W * UNROLL;
2665
2666    unsafe {
2667        let len = a.len();
2668        let a_ptr = a.as_ptr();
2669        let mut acc = [_mm_setzero_pd(); UNROLL];
2670
2671        let n_blocks = len / BLOCK;
2672        for i in 0..n_blocks {
2673            let base = i * BLOCK;
2674            for (u, acc) in acc.iter_mut().enumerate() {
2675                let v = _mm_loadu_ps(a_ptr.add(base + u * W));
2676                let lo = _mm_cvtps_pd(v);
2677                let hi = _mm_cvtps_pd(_mm_movehl_ps(v, v));
2678                *acc = _mm_add_pd(*acc, _mm_add_pd(lo, hi));
2679            }
2680        }
2681
2682        let mut total = acc[0];
2683        for acc in &acc[1..] {
2684            total = _mm_add_pd(total, *acc);
2685        }
2686
2687        let mut i = n_blocks * BLOCK;
2688        while i + W <= len {
2689            let v = _mm_loadu_ps(a_ptr.add(i));
2690            let lo = _mm_cvtps_pd(v);
2691            let hi = _mm_cvtps_pd(_mm_movehl_ps(v, v));
2692            total = _mm_add_pd(total, _mm_add_pd(lo, hi));
2693            i += W;
2694        }
2695
2696        let mut sum = hsum_sse_f64(total);
2697        while i < len {
2698            sum += a[i] as f64;
2699            i += 1;
2700        }
2701        sum
2702    }
2703}
2704
2705/// Widening sum of a slice of f32, accumulated in f64 (128-bit, NEON)
2706///
2707/// ### Params
2708///
2709/// * `a` - The slice of f32 values to sum.
2710///
2711/// ### Returns
2712///
2713/// Sum, in f64
2714#[cfg(target_arch = "aarch64")]
2715#[target_feature(enable = "neon")]
2716unsafe fn sum_widen_neon_f32(a: &[f32]) -> f64 {
2717    const W: usize = 4;
2718    const BLOCK: usize = W * UNROLL;
2719
2720    unsafe {
2721        let len = a.len();
2722        let a_ptr = a.as_ptr();
2723        let mut acc = [vdupq_n_f64(0.0); UNROLL];
2724
2725        let n_blocks = len / BLOCK;
2726        for i in 0..n_blocks {
2727            let base = i * BLOCK;
2728            for (u, acc) in acc.iter_mut().enumerate() {
2729                let v = vld1q_f32(a_ptr.add(base + u * W));
2730                let lo = vcvt_f64_f32(vget_low_f32(v));
2731                let hi = vcvt_high_f64_f32(v);
2732                *acc = vaddq_f64(*acc, vaddq_f64(lo, hi));
2733            }
2734        }
2735
2736        let mut total = acc[0];
2737        for acc in &acc[1..] {
2738            total = vaddq_f64(total, *acc);
2739        }
2740
2741        let mut i = n_blocks * BLOCK;
2742        while i + W <= len {
2743            let v = vld1q_f32(a_ptr.add(i));
2744            let lo = vcvt_f64_f32(vget_low_f32(v));
2745            let hi = vcvt_high_f64_f32(v);
2746            total = vaddq_f64(total, vaddq_f64(lo, hi));
2747            i += W;
2748        }
2749
2750        let mut sum = vaddvq_f64(total);
2751        while i < len {
2752            sum += a[i] as f64;
2753            i += 1;
2754        }
2755        sum
2756    }
2757}
2758
2759/// Widening sum of a slice of f32, accumulated in f64 (256-bit)
2760///
2761/// ### Params
2762///
2763/// * `a` - The slice of f32 values to sum.
2764///
2765/// ### Returns
2766///
2767/// Sum, in f64
2768#[cfg(target_arch = "x86_64")]
2769#[target_feature(enable = "avx2", enable = "fma")]
2770unsafe fn sum_widen_avx2_f32(a: &[f32]) -> f64 {
2771    const W: usize = 8;
2772    const BLOCK: usize = W * UNROLL;
2773
2774    unsafe {
2775        let len = a.len();
2776        let a_ptr = a.as_ptr();
2777        let mut acc = [_mm256_setzero_pd(); UNROLL];
2778
2779        let n_blocks = len / BLOCK;
2780        for i in 0..n_blocks {
2781            let base = i * BLOCK;
2782            for (u, acc) in acc.iter_mut().enumerate() {
2783                let p = a_ptr.add(base + u * W);
2784                let lo = _mm256_cvtps_pd(_mm_loadu_ps(p));
2785                let hi = _mm256_cvtps_pd(_mm_loadu_ps(p.add(4)));
2786                *acc = _mm256_add_pd(*acc, _mm256_add_pd(lo, hi));
2787            }
2788        }
2789
2790        let mut total = acc[0];
2791        for acc in &acc[1..] {
2792            total = _mm256_add_pd(total, *acc);
2793        }
2794
2795        let mut i = n_blocks * BLOCK;
2796        while i + W <= len {
2797            let p = a_ptr.add(i);
2798            let lo = _mm256_cvtps_pd(_mm_loadu_ps(p));
2799            let hi = _mm256_cvtps_pd(_mm_loadu_ps(p.add(4)));
2800            total = _mm256_add_pd(total, _mm256_add_pd(lo, hi));
2801            i += W;
2802        }
2803
2804        let mut sum = hsum_avx_f64(total);
2805        while i < len {
2806            sum += a[i] as f64;
2807            i += 1;
2808        }
2809        sum
2810    }
2811}
2812
2813/// Widening sum of a slice of f32, accumulated in f64 (512-bit)
2814///
2815/// ### Params
2816///
2817/// * `a` - The slice of f32 values to sum.
2818///
2819/// ### Returns
2820///
2821/// Sum, in f64
2822#[cfg(target_arch = "x86_64")]
2823#[target_feature(enable = "avx512f")]
2824unsafe fn sum_widen_avx512_f32(a: &[f32]) -> f64 {
2825    const W: usize = 16;
2826    const BLOCK: usize = W * UNROLL;
2827
2828    unsafe {
2829        let len = a.len();
2830        let a_ptr = a.as_ptr();
2831        let mut acc = [_mm512_setzero_pd(); UNROLL];
2832
2833        let n_blocks = len / BLOCK;
2834        for i in 0..n_blocks {
2835            let base = i * BLOCK;
2836            for (u, acc) in acc.iter_mut().enumerate() {
2837                let p = a_ptr.add(base + u * W);
2838                let lo = _mm512_cvtps_pd(_mm256_loadu_ps(p));
2839                let hi = _mm512_cvtps_pd(_mm256_loadu_ps(p.add(8)));
2840                *acc = _mm512_add_pd(*acc, _mm512_add_pd(lo, hi));
2841            }
2842        }
2843
2844        let mut total = acc[0];
2845        for acc in &acc[1..] {
2846            total = _mm512_add_pd(total, *acc);
2847        }
2848
2849        let mut i = n_blocks * BLOCK;
2850        while i + W <= len {
2851            let p = a_ptr.add(i);
2852            let lo = _mm512_cvtps_pd(_mm256_loadu_ps(p));
2853            let hi = _mm512_cvtps_pd(_mm256_loadu_ps(p.add(8)));
2854            total = _mm512_add_pd(total, _mm512_add_pd(lo, hi));
2855            i += W;
2856        }
2857
2858        let mut sum = _mm512_reduce_add_pd(total);
2859        while i < len {
2860            sum += a[i] as f64;
2861            i += 1;
2862        }
2863        sum
2864    }
2865}
2866
2867/// Widening sum of a slice of f32, accumulated in f64 (dispatch)
2868///
2869/// ### Params
2870///
2871/// * `a` - The slice of f32 values to sum.
2872///
2873/// ### Returns
2874///
2875/// Sum, in f64
2876#[inline]
2877pub fn sum_widen_simd_f32(a: &[f32]) -> f64 {
2878    #[cfg(target_arch = "x86_64")]
2879    // SAFETY: see `sum_squares_simd_f32`.
2880    unsafe {
2881        match detect_simd_level() {
2882            SimdLevel::Avx512 => sum_widen_avx512_f32(a),
2883            SimdLevel::Avx2 => sum_widen_avx2_f32(a),
2884            SimdLevel::Sse => sum_widen_sse_f32(a),
2885            SimdLevel::Scalar => sum_widen_scalar_f32(a),
2886        }
2887    }
2888
2889    #[cfg(target_arch = "aarch64")]
2890    // SAFETY: NEON is baseline on aarch64, so the feature is unconditionally
2891    // present and `detect_simd_level` always reports `Sse` here.
2892    unsafe {
2893        match detect_simd_level() {
2894            SimdLevel::Sse => sum_widen_neon_f32(a),
2895            _ => sum_widen_scalar_f32(a),
2896        }
2897    }
2898
2899    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
2900    sum_widen_scalar_f32(a)
2901}
2902
2903//////////////
2904// Variance //
2905//////////////
2906
2907/// Widening sum of squared deviations of a slice of f32 (scalar)
2908///
2909/// ### Params
2910///
2911/// * `a` - The slice of f32 values to calculate variance for.
2912/// * `mean` - The mean of the values in `a`, in f64.
2913///
2914/// ### Returns
2915///
2916/// Sum of the squared deviations from the mean, in f64
2917#[inline(always)]
2918fn sum_squared_dev_widen_scalar_f32(a: &[f32], mean: f64) -> f64 {
2919    a.iter()
2920        .map(|&x| {
2921            let d = x as f64 - mean;
2922            d * d
2923        })
2924        .sum()
2925}
2926
2927/// Widening sum of squared deviations of a slice of f32 (128-bit, SSE2)
2928///
2929/// ### Params
2930///
2931/// * `a` - The slice of f32 values to calculate variance for.
2932/// * `mean` - The mean of the values in `a`, in f64.
2933///
2934/// ### Returns
2935///
2936/// Sum of the squared deviations from the mean, in f64
2937#[cfg(target_arch = "x86_64")]
2938#[target_feature(enable = "sse2")]
2939unsafe fn sum_squared_dev_widen_sse_f32(a: &[f32], mean: f64) -> f64 {
2940    const W: usize = 4;
2941    const BLOCK: usize = W * UNROLL;
2942
2943    unsafe {
2944        let len = a.len();
2945        let a_ptr = a.as_ptr();
2946        let mean_vec = _mm_set1_pd(mean);
2947        let mut acc = [_mm_setzero_pd(); UNROLL];
2948
2949        let n_blocks = len / BLOCK;
2950        for i in 0..n_blocks {
2951            let base = i * BLOCK;
2952            for (u, acc) in acc.iter_mut().enumerate() {
2953                let v = _mm_loadu_ps(a_ptr.add(base + u * W));
2954                let lo = _mm_sub_pd(_mm_cvtps_pd(v), mean_vec);
2955                let hi = _mm_sub_pd(_mm_cvtps_pd(_mm_movehl_ps(v, v)), mean_vec);
2956                *acc = _mm_add_pd(*acc, _mm_add_pd(_mm_mul_pd(lo, lo), _mm_mul_pd(hi, hi)));
2957            }
2958        }
2959
2960        let mut total = acc[0];
2961        for acc in &acc[1..] {
2962            total = _mm_add_pd(total, *acc);
2963        }
2964
2965        let mut i = n_blocks * BLOCK;
2966        while i + W <= len {
2967            let v = _mm_loadu_ps(a_ptr.add(i));
2968            let lo = _mm_sub_pd(_mm_cvtps_pd(v), mean_vec);
2969            let hi = _mm_sub_pd(_mm_cvtps_pd(_mm_movehl_ps(v, v)), mean_vec);
2970            total = _mm_add_pd(total, _mm_add_pd(_mm_mul_pd(lo, lo), _mm_mul_pd(hi, hi)));
2971            i += W;
2972        }
2973
2974        let mut sum = hsum_sse_f64(total);
2975        while i < len {
2976            let d = a[i] as f64 - mean;
2977            sum += d * d;
2978            i += 1;
2979        }
2980        sum
2981    }
2982}
2983
2984/// Widening sum of squared deviations of a slice of f32 (128-bit, NEON)
2985///
2986/// ### Params
2987///
2988/// * `a` - The slice of f32 values to calculate variance for.
2989/// * `mean` - The mean of the values in `a`, in f64.
2990///
2991/// ### Returns
2992///
2993/// Sum of the squared deviations from the mean, in f64
2994#[cfg(target_arch = "aarch64")]
2995#[target_feature(enable = "neon")]
2996unsafe fn sum_squared_dev_widen_neon_f32(a: &[f32], mean: f64) -> f64 {
2997    const W: usize = 4;
2998    const BLOCK: usize = W * UNROLL;
2999
3000    unsafe {
3001        let len = a.len();
3002        let a_ptr = a.as_ptr();
3003        let mean_vec = vdupq_n_f64(mean);
3004        let mut acc = [vdupq_n_f64(0.0); UNROLL];
3005
3006        let n_blocks = len / BLOCK;
3007        for i in 0..n_blocks {
3008            let base = i * BLOCK;
3009            for (u, acc) in acc.iter_mut().enumerate() {
3010                let v = vld1q_f32(a_ptr.add(base + u * W));
3011                let lo = vsubq_f64(vcvt_f64_f32(vget_low_f32(v)), mean_vec);
3012                let hi = vsubq_f64(vcvt_high_f64_f32(v), mean_vec);
3013                *acc = vfmaq_f64(vfmaq_f64(*acc, lo, lo), hi, hi);
3014            }
3015        }
3016
3017        let mut total = acc[0];
3018        for acc in &acc[1..] {
3019            total = vaddq_f64(total, *acc);
3020        }
3021
3022        let mut i = n_blocks * BLOCK;
3023        while i + W <= len {
3024            let v = vld1q_f32(a_ptr.add(i));
3025            let lo = vsubq_f64(vcvt_f64_f32(vget_low_f32(v)), mean_vec);
3026            let hi = vsubq_f64(vcvt_high_f64_f32(v), mean_vec);
3027            total = vfmaq_f64(vfmaq_f64(total, lo, lo), hi, hi);
3028            i += W;
3029        }
3030
3031        let mut sum = vaddvq_f64(total);
3032        while i < len {
3033            let d = a[i] as f64 - mean;
3034            sum += d * d;
3035            i += 1;
3036        }
3037        sum
3038    }
3039}
3040
3041/// Widening sum of squared deviations of a slice of f32 (256-bit)
3042///
3043/// ### Params
3044///
3045/// * `a` - The slice of f32 values to calculate variance for.
3046/// * `mean` - The mean of the values in `a`, in f64.
3047///
3048/// ### Returns
3049///
3050/// Sum of the squared deviations from the mean, in f64
3051#[cfg(target_arch = "x86_64")]
3052#[target_feature(enable = "avx2", enable = "fma")]
3053unsafe fn sum_squared_dev_widen_avx2_f32(a: &[f32], mean: f64) -> f64 {
3054    const W: usize = 8;
3055    const BLOCK: usize = W * UNROLL;
3056
3057    unsafe {
3058        let len = a.len();
3059        let a_ptr = a.as_ptr();
3060        let mean_vec = _mm256_set1_pd(mean);
3061        let mut acc = [_mm256_setzero_pd(); UNROLL];
3062
3063        let n_blocks = len / BLOCK;
3064        for i in 0..n_blocks {
3065            let base = i * BLOCK;
3066            for (u, acc) in acc.iter_mut().enumerate() {
3067                let p = a_ptr.add(base + u * W);
3068                let lo = _mm256_sub_pd(_mm256_cvtps_pd(_mm_loadu_ps(p)), mean_vec);
3069                let hi = _mm256_sub_pd(_mm256_cvtps_pd(_mm_loadu_ps(p.add(4))), mean_vec);
3070                *acc = _mm256_fmadd_pd(hi, hi, _mm256_fmadd_pd(lo, lo, *acc));
3071            }
3072        }
3073
3074        let mut total = acc[0];
3075        for acc in &acc[1..] {
3076            total = _mm256_add_pd(total, *acc);
3077        }
3078
3079        let mut i = n_blocks * BLOCK;
3080        while i + W <= len {
3081            let p = a_ptr.add(i);
3082            let lo = _mm256_sub_pd(_mm256_cvtps_pd(_mm_loadu_ps(p)), mean_vec);
3083            let hi = _mm256_sub_pd(_mm256_cvtps_pd(_mm_loadu_ps(p.add(4))), mean_vec);
3084            total = _mm256_fmadd_pd(hi, hi, _mm256_fmadd_pd(lo, lo, total));
3085            i += W;
3086        }
3087
3088        let mut sum = hsum_avx_f64(total);
3089        while i < len {
3090            let d = a[i] as f64 - mean;
3091            sum += d * d;
3092            i += 1;
3093        }
3094        sum
3095    }
3096}
3097
3098/// Widening sum of squared deviations of a slice of f32 (512-bit)
3099///
3100/// ### Params
3101///
3102/// * `a` - The slice of f32 values to calculate variance for.
3103/// * `mean` - The mean of the values in `a`, in f64.
3104///
3105/// ### Returns
3106///
3107/// Sum of the squared deviations from the mean, in f64
3108#[cfg(target_arch = "x86_64")]
3109#[target_feature(enable = "avx512f")]
3110unsafe fn sum_squared_dev_widen_avx512_f32(a: &[f32], mean: f64) -> f64 {
3111    const W: usize = 16;
3112    const BLOCK: usize = W * UNROLL;
3113
3114    unsafe {
3115        let len = a.len();
3116        let a_ptr = a.as_ptr();
3117        let mean_vec = _mm512_set1_pd(mean);
3118        let mut acc = [_mm512_setzero_pd(); UNROLL];
3119
3120        let n_blocks = len / BLOCK;
3121        for i in 0..n_blocks {
3122            let base = i * BLOCK;
3123            for (u, acc) in acc.iter_mut().enumerate() {
3124                let p = a_ptr.add(base + u * W);
3125                let lo = _mm512_sub_pd(_mm512_cvtps_pd(_mm256_loadu_ps(p)), mean_vec);
3126                let hi = _mm512_sub_pd(_mm512_cvtps_pd(_mm256_loadu_ps(p.add(8))), mean_vec);
3127                *acc = _mm512_fmadd_pd(hi, hi, _mm512_fmadd_pd(lo, lo, *acc));
3128            }
3129        }
3130
3131        let mut total = acc[0];
3132        for acc in &acc[1..] {
3133            total = _mm512_add_pd(total, *acc);
3134        }
3135
3136        let mut i = n_blocks * BLOCK;
3137        while i + W <= len {
3138            let p = a_ptr.add(i);
3139            let lo = _mm512_sub_pd(_mm512_cvtps_pd(_mm256_loadu_ps(p)), mean_vec);
3140            let hi = _mm512_sub_pd(_mm512_cvtps_pd(_mm256_loadu_ps(p.add(8))), mean_vec);
3141            total = _mm512_fmadd_pd(hi, hi, _mm512_fmadd_pd(lo, lo, total));
3142            i += W;
3143        }
3144
3145        let mut sum = _mm512_reduce_add_pd(total);
3146        while i < len {
3147            let d = a[i] as f64 - mean;
3148            sum += d * d;
3149            i += 1;
3150        }
3151        sum
3152    }
3153}
3154
3155/// Widening sum of squared deviations of a slice of f32 (dispatch)
3156///
3157/// ### Params
3158///
3159/// * `a` - The slice of f32 values to calculate variance for.
3160/// * `mean` - The mean of the values in `a`, in f64.
3161///
3162/// ### Returns
3163///
3164/// Sum of the squared deviations from the mean, in f64
3165#[inline]
3166pub fn sum_squared_dev_widen_simd_f32(a: &[f32], mean: f64) -> f64 {
3167    #[cfg(target_arch = "x86_64")]
3168    // SAFETY: see `sum_squares_simd_f32`.
3169    unsafe {
3170        match detect_simd_level() {
3171            SimdLevel::Avx512 => sum_squared_dev_widen_avx512_f32(a, mean),
3172            SimdLevel::Avx2 => sum_squared_dev_widen_avx2_f32(a, mean),
3173            SimdLevel::Sse => sum_squared_dev_widen_sse_f32(a, mean),
3174            SimdLevel::Scalar => sum_squared_dev_widen_scalar_f32(a, mean),
3175        }
3176    }
3177
3178    #[cfg(target_arch = "aarch64")]
3179    // SAFETY: see `sum_widen_simd_f32`.
3180    unsafe {
3181        match detect_simd_level() {
3182            SimdLevel::Sse => sum_squared_dev_widen_neon_f32(a, mean),
3183            _ => sum_squared_dev_widen_scalar_f32(a, mean),
3184        }
3185    }
3186
3187    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
3188    sum_squared_dev_widen_scalar_f32(a, mean)
3189}
3190
3191/////////////////
3192// Shifted exp //
3193/////////////////
3194
3195/// Shifted exponential with its sum (scalar).
3196///
3197/// ### Params
3198///
3199/// * `src` - Input slice
3200/// * `shift` - Subtracted before the exponential
3201/// * `dst` - Output, same length as `src`
3202///
3203/// ### Returns
3204///
3205/// The sum of `dst`
3206#[inline(always)]
3207fn exp_shift_sum_scalar<T: num_traits::Float + std::iter::Sum>(
3208    src: &[T],
3209    shift: T,
3210    dst: &mut [T],
3211) -> T {
3212    let mut total = T::zero();
3213    for (d, &x) in dst.iter_mut().zip(src) {
3214        *d = (x - shift).exp();
3215        total = total + *d;
3216    }
3217    total
3218}
3219
3220/// Lanes per pass of [`exp_shift_sum_poly_f32`]; a fixed-size array lets
3221/// LLVM vectorise the polynomial at whatever width the target has.
3222const EXP_LANES: usize = 8;
3223
3224/// `exp(x)` for one lane, branch-free so that a loop over it vectorises.
3225///
3226/// Same scheme as `wide`'s `f32x4::exp`: Cody-Waite range reduction
3227/// `x = r ln2 + f`, the Taylor series of `exp(f)` to `f^7 / 5040`, scaled by
3228/// `2^r` through the exponent bits. Written per lane rather than through
3229/// `wide`, whose NEON lowering measured slower than libm. Inputs are clamped
3230/// to `[-87.3, 88]`, so very negative inputs give about `1e-38` rather than a
3231/// subnormal or zero.
3232///
3233/// ### Params
3234///
3235/// * `x` - Input
3236///
3237/// ### Returns
3238///
3239/// `exp(x)` to a few ulp
3240#[inline(always)]
3241fn exp_poly_f32(x: f32) -> f32 {
3242    const LN2_HI: f32 = 0.693_359_4;
3243    const LN2_LO: f32 = -2.121_944_4e-4;
3244    let x = x.clamp(-87.3, 88.0);
3245    let r = (x * std::f32::consts::LOG2_E).round();
3246    let f = x - r * LN2_HI - r * LN2_LO;
3247    let p = 1.0 / 5040.0_f32;
3248    let p = p * f + 1.0 / 720.0;
3249    let p = p * f + 1.0 / 120.0;
3250    let p = p * f + 1.0 / 24.0;
3251    let p = p * f + 1.0 / 6.0;
3252    let p = p * f + 0.5;
3253    let e = p * f * f + f + 1.0;
3254    e * f32::from_bits(((r as i32 + 127) << 23) as u32)
3255}
3256
3257/// Shifted exponential with its sum, via [`exp_poly_f32`] in fixed lanes.
3258///
3259/// ### Params
3260///
3261/// * `src` - Input slice
3262/// * `shift` - Subtracted before the exponential
3263/// * `dst` - Output, same length as `src`
3264///
3265/// ### Returns
3266///
3267/// The sum of `dst`
3268#[inline(always)]
3269fn exp_shift_sum_poly_f32(src: &[f32], shift: f32, dst: &mut [f32]) -> f32 {
3270    let mut acc = [0.0f32; EXP_LANES];
3271    let mut src_chunks = src.chunks_exact(EXP_LANES);
3272    let mut dst_chunks = dst.chunks_exact_mut(EXP_LANES);
3273    for (d, s) in (&mut dst_chunks).zip(&mut src_chunks) {
3274        for j in 0..EXP_LANES {
3275            d[j] = exp_poly_f32(s[j] - shift);
3276            acc[j] += d[j];
3277        }
3278    }
3279    let mut total: f32 = acc.iter().sum();
3280    for (d, &x) in dst_chunks
3281        .into_remainder()
3282        .iter_mut()
3283        .zip(src_chunks.remainder())
3284    {
3285        *d = exp_poly_f32(x - shift);
3286        total += *d;
3287    }
3288    total
3289}
3290
3291/// Shifted exponential `dst = exp(src - shift)` with its sum.
3292///
3293/// ### Params
3294///
3295/// * `src` - Input slice
3296/// * `shift` - Subtracted before the exponential
3297/// * `dst` - Output, same length as `src`
3298///
3299/// ### Returns
3300///
3301/// The sum of `dst`
3302#[inline]
3303pub fn exp_shift_sum_simd_f32(src: &[f32], shift: f32, dst: &mut [f32]) -> f32 {
3304    exp_shift_sum_poly_f32(src, shift, dst)
3305}
3306
3307///////////
3308// Tests //
3309///////////
3310
3311#[cfg(test)]
3312mod tests {
3313    use super::*;
3314    use approx::assert_relative_eq;
3315    use rand::prelude::*;
3316    use rand::rngs::StdRng;
3317
3318    /// Lengths sweeping the 2-, 4-, 8- and 16-wide boundaries and the unrolled
3319    /// block sizes (4x4 = 16, 4x8 = 32, 4x16 = 64), so both the block loop and
3320    /// every tail path get exercised.
3321    const LENGTHS: [usize; 22] = [
3322        1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 100, 127, 257, 666, 1000,
3323    ];
3324
3325    /// The vectorised subtract-and-argmin must agree with the scalar scan it
3326    /// replaced, exactly.
3327    ///
3328    /// The low-cardinality case is the important one: ties are where a
3329    /// lane-wise reduction is most likely to disagree, and with `UNROLL`
3330    /// independent accumulators a tie can now also straddle two accumulators
3331    /// whose minima came from different blocks.
3332    #[test]
3333    fn test_argmin_diff_simd_matches_scalar() {
3334        let mut rng = StdRng::seed_from_u64(42);
3335
3336        for len in LENGTHS {
3337            for tied in [false, true] {
3338                let (a, b): (Vec<f32>, Vec<f32>) = (0..len)
3339                    .map(|_| {
3340                        if tied {
3341                            // Few distinct differences, so ties are frequent.
3342                            (rng.random_range(0..3) as f32, 0.0f32)
3343                        } else {
3344                            (
3345                                rng.random::<f32>() * 2.0 - 1.0,
3346                                rng.random::<f32>() * 2.0 - 1.0,
3347                            )
3348                        }
3349                    })
3350                    .unzip();
3351
3352                let (simd_idx, simd_val) = argmin_diff_simd_f32(&a, &b);
3353                let (scalar_idx, scalar_val) = argmin_diff_scalar_f32(&a, &b);
3354
3355                assert_eq!(
3356                    simd_idx, scalar_idx,
3357                    "index mismatch at len {} (tied {:?})",
3358                    len, tied
3359                );
3360                assert_eq!(
3361                    simd_val, scalar_val,
3362                    "value mismatch at len {} (tied {:?})",
3363                    len, tied
3364                );
3365            }
3366        }
3367    }
3368
3369    /// Every width must agree with the scalar reference regardless of what the
3370    /// runtime dispatch picks on this machine, so the arms that are not
3371    /// selected here still get covered wherever the hardware allows it.
3372    #[test]
3373    fn test_argmin_diff_widths_agree() {
3374        let mut rng = StdRng::seed_from_u64(7);
3375        for len in LENGTHS {
3376            for tied in [false, true] {
3377                let (a, b): (Vec<f32>, Vec<f32>) = (0..len)
3378                    .map(|_| {
3379                        if tied {
3380                            (rng.random_range(0..3) as f32, 0.0f32)
3381                        } else {
3382                            (rng.random::<f32>(), rng.random::<f32>())
3383                        }
3384                    })
3385                    .unzip();
3386
3387                let reference = argmin_diff_scalar_f32(&a, &b);
3388
3389                #[cfg(target_arch = "aarch64")]
3390                unsafe {
3391                    assert_eq!(
3392                        argmin_diff_neon_f32(&a, &b),
3393                        reference,
3394                        "neon at len {} (tied {:?})",
3395                        len,
3396                        tied
3397                    );
3398                }
3399
3400                #[cfg(target_arch = "x86_64")]
3401                unsafe {
3402                    if is_x86_feature_detected!("sse4.1") {
3403                        assert_eq!(
3404                            argmin_diff_sse_f32(&a, &b),
3405                            reference,
3406                            "sse at len {} (tied {:?})",
3407                            len,
3408                            tied
3409                        );
3410                    }
3411                    if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
3412                        assert_eq!(
3413                            argmin_diff_avx2_f32(&a, &b),
3414                            reference,
3415                            "avx2 at len {} (tied {:?})",
3416                            len,
3417                            tied
3418                        );
3419                    }
3420                    if is_x86_feature_detected!("avx512f") {
3421                        assert_eq!(
3422                            argmin_diff_avx512_f32(&a, &b),
3423                            reference,
3424                            "avx512 at len {} (tied {:?})",
3425                            len,
3426                            tied
3427                        );
3428                    }
3429                }
3430            }
3431        }
3432    }
3433
3434    /// Every reduction arm must agree with its scalar reference on this
3435    /// machine. Horizontal order differs between widths, so this is a
3436    /// tolerance check rather than an exact one; the argmin above is the only
3437    /// kernel where bit-exact agreement is required.
3438    ///
3439    /// On aarch64 only the 128-bit arm runs; the x86 arms are covered wherever
3440    /// CI provides the hardware.
3441    #[test]
3442    fn test_reduction_widths_agree() {
3443        let mut rng = StdRng::seed_from_u64(99);
3444
3445        for len in LENGTHS {
3446            let a32: Vec<f32> = (0..len).map(|_| rng.random::<f32>() * 2.0 - 1.0).collect();
3447            let b32: Vec<f32> = (0..len).map(|_| rng.random::<f32>() * 2.0 - 1.0).collect();
3448            let a64: Vec<f64> = a32.iter().map(|&x| x as f64).collect();
3449            let b64: Vec<f64> = b32.iter().map(|&x| x as f64).collect();
3450            let mean32 = sum_scalar_f32(&a32) / len as f32;
3451            let mean64 = sum_scalar_f64(&a64) / len as f64;
3452
3453            assert_relative_eq!(
3454                sum_squares_sse_f32(&a32),
3455                sum_squares_scalar_f32(&a32),
3456                epsilon = 1e-4,
3457                max_relative = 1e-5
3458            );
3459            assert_relative_eq!(
3460                dot_sse_f32(&a32, &b32),
3461                dot_scalar_f32(&a32, &b32),
3462                epsilon = 1e-4,
3463                max_relative = 1e-5
3464            );
3465            assert_relative_eq!(
3466                sum_sse_f32(&a32),
3467                sum_scalar_f32(&a32),
3468                epsilon = 1e-4,
3469                max_relative = 1e-5
3470            );
3471            assert_relative_eq!(
3472                sum_squared_dev_sse_f32(&a32, mean32),
3473                sum_squared_dev_scalar_f32(&a32, mean32),
3474                epsilon = 1e-4,
3475                max_relative = 1e-5
3476            );
3477            assert_relative_eq!(
3478                dot_sse_f64(&a64, &b64),
3479                dot_scalar_f64(&a64, &b64),
3480                epsilon = 1e-12,
3481                max_relative = 1e-12
3482            );
3483            assert_relative_eq!(
3484                sum_sse_f64(&a64),
3485                sum_scalar_f64(&a64),
3486                epsilon = 1e-12,
3487                max_relative = 1e-12
3488            );
3489            assert_relative_eq!(
3490                sum_squared_dev_sse_f64(&a64, mean64),
3491                sum_squared_dev_scalar_f64(&a64, mean64),
3492                epsilon = 1e-12,
3493                max_relative = 1e-12
3494            );
3495
3496            #[cfg(target_arch = "x86_64")]
3497            unsafe {
3498                if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
3499                    assert_relative_eq!(
3500                        sum_squares_avx2_f32(&a32),
3501                        sum_squares_scalar_f32(&a32),
3502                        epsilon = 1e-4,
3503                        max_relative = 1e-5
3504                    );
3505                    assert_relative_eq!(
3506                        dot_avx2_f32(&a32, &b32),
3507                        dot_scalar_f32(&a32, &b32),
3508                        epsilon = 1e-4,
3509                        max_relative = 1e-5
3510                    );
3511                    assert_relative_eq!(
3512                        sum_avx2_f32(&a32),
3513                        sum_scalar_f32(&a32),
3514                        epsilon = 1e-4,
3515                        max_relative = 1e-5
3516                    );
3517                    assert_relative_eq!(
3518                        sum_squared_dev_avx2_f32(&a32, mean32),
3519                        sum_squared_dev_scalar_f32(&a32, mean32),
3520                        epsilon = 1e-4,
3521                        max_relative = 1e-5
3522                    );
3523                    assert_relative_eq!(
3524                        dot_avx2_f64(&a64, &b64),
3525                        dot_scalar_f64(&a64, &b64),
3526                        epsilon = 1e-12,
3527                        max_relative = 1e-12
3528                    );
3529                    assert_relative_eq!(
3530                        sum_avx2_f64(&a64),
3531                        sum_scalar_f64(&a64),
3532                        epsilon = 1e-12,
3533                        max_relative = 1e-12
3534                    );
3535                    assert_relative_eq!(
3536                        sum_squared_dev_avx2_f64(&a64, mean64),
3537                        sum_squared_dev_scalar_f64(&a64, mean64),
3538                        epsilon = 1e-12,
3539                        max_relative = 1e-12
3540                    );
3541                }
3542
3543                if is_x86_feature_detected!("avx512f") {
3544                    assert_relative_eq!(
3545                        sum_squares_avx512_f32(&a32),
3546                        sum_squares_scalar_f32(&a32),
3547                        epsilon = 1e-4,
3548                        max_relative = 1e-5
3549                    );
3550                    assert_relative_eq!(
3551                        dot_avx512_f32(&a32, &b32),
3552                        dot_scalar_f32(&a32, &b32),
3553                        epsilon = 1e-4,
3554                        max_relative = 1e-5
3555                    );
3556                    assert_relative_eq!(
3557                        sum_avx512_f32(&a32),
3558                        sum_scalar_f32(&a32),
3559                        epsilon = 1e-4,
3560                        max_relative = 1e-5
3561                    );
3562                    assert_relative_eq!(
3563                        sum_squared_dev_avx512_f32(&a32, mean32),
3564                        sum_squared_dev_scalar_f32(&a32, mean32),
3565                        epsilon = 1e-4,
3566                        max_relative = 1e-5
3567                    );
3568                    assert_relative_eq!(
3569                        dot_avx512_f64(&a64, &b64),
3570                        dot_scalar_f64(&a64, &b64),
3571                        epsilon = 1e-12,
3572                        max_relative = 1e-12
3573                    );
3574                    assert_relative_eq!(
3575                        sum_avx512_f64(&a64),
3576                        sum_scalar_f64(&a64),
3577                        epsilon = 1e-12,
3578                        max_relative = 1e-12
3579                    );
3580                    assert_relative_eq!(
3581                        sum_squared_dev_avx512_f64(&a64, mean64),
3582                        sum_squared_dev_scalar_f64(&a64, mean64),
3583                        epsilon = 1e-12,
3584                        max_relative = 1e-12
3585                    );
3586                }
3587            }
3588        }
3589    }
3590
3591    /// Every widening arm must agree with the widened scalar reference.
3592    ///
3593    /// Same tolerance as the f64 reductions above, since the accumulation is
3594    /// f64 in every arm; only the load and convert are f32.
3595    #[test]
3596    fn test_widening_reductions_match_f64_reference() {
3597        let mut rng = StdRng::seed_from_u64(7);
3598
3599        for len in LENGTHS {
3600            let a: Vec<f32> = (0..len).map(|_| rng.random::<f32>() * 2.0 - 1.0).collect();
3601            let mean = sum_widen_scalar_f32(&a) / len as f64;
3602            let sum_ref = sum_widen_scalar_f32(&a);
3603            let dev_ref = sum_squared_dev_widen_scalar_f32(&a, mean);
3604
3605            #[cfg(target_arch = "aarch64")]
3606            // SAFETY: NEON is baseline on aarch64.
3607            unsafe {
3608                assert_relative_eq!(sum_widen_neon_f32(&a), sum_ref, epsilon = 1e-12);
3609                assert_relative_eq!(
3610                    sum_squared_dev_widen_neon_f32(&a, mean),
3611                    dev_ref,
3612                    epsilon = 1e-12
3613                );
3614            }
3615
3616            #[cfg(target_arch = "x86_64")]
3617            // SAFETY: SSE2 is baseline on x86_64, the wider arms are probed.
3618            unsafe {
3619                assert_relative_eq!(sum_widen_sse_f32(&a), sum_ref, epsilon = 1e-12);
3620                assert_relative_eq!(
3621                    sum_squared_dev_widen_sse_f32(&a, mean),
3622                    dev_ref,
3623                    epsilon = 1e-12
3624                );
3625
3626                if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
3627                    assert_relative_eq!(sum_widen_avx2_f32(&a), sum_ref, epsilon = 1e-12);
3628                    assert_relative_eq!(
3629                        sum_squared_dev_widen_avx2_f32(&a, mean),
3630                        dev_ref,
3631                        epsilon = 1e-12
3632                    );
3633                }
3634
3635                if is_x86_feature_detected!("avx512f") {
3636                    assert_relative_eq!(sum_widen_avx512_f32(&a), sum_ref, epsilon = 1e-12);
3637                    assert_relative_eq!(
3638                        sum_squared_dev_widen_avx512_f32(&a, mean),
3639                        dev_ref,
3640                        epsilon = 1e-12
3641                    );
3642                }
3643            }
3644        }
3645    }
3646
3647    /// The reason the widening arms exist: a metacell-sized column of counts
3648    /// sums past `2^24`, where an f32 accumulator starts dropping entries
3649    /// whole. The widened sum stays exact; the f32 one is off by orders of
3650    /// magnitude more than the tolerance any caller would set.
3651    #[test]
3652    fn test_widening_sum_survives_large_counts() {
3653        let mut rng = StdRng::seed_from_u64(11);
3654        let values: Vec<f32> = (0..50_000)
3655            .map(|_| (rng.random::<f32>() * 1e5).round())
3656            .collect();
3657
3658        let exact: f64 = values.iter().map(|&x| x as f64).sum();
3659        let widened = sum_widen_simd_f32(&values);
3660        let naive = values.iter().sum::<f32>() as f64;
3661
3662        assert_relative_eq!(widened, exact, max_relative = 1e-12);
3663        assert!(
3664            (naive - exact).abs() > (widened - exact).abs() * 1e3,
3665            "f32 accumulation was expected to drift badly: naive {naive}, exact {exact}"
3666        );
3667    }
3668
3669    /// Every axpy arm must agree with the scalar reference elementwise.
3670    ///
3671    /// Tolerance rather than equality because the vector arms fuse the multiply
3672    /// and add while the scalar loop may or may not, depending on what LLVM
3673    /// contracts, so the intermediate rounding can differ by an ulp.
3674    ///
3675    /// The lengths sweep every lane boundary, so the scalar tail of each arm is
3676    /// exercised as well as its vector body.
3677    #[test]
3678    fn test_axpy_widths_agree() {
3679        let mut rng = StdRng::seed_from_u64(2024);
3680
3681        for len in LENGTHS {
3682            let x32: Vec<f32> = (0..len).map(|_| rng.random::<f32>() * 2.0 - 1.0).collect();
3683            let y32: Vec<f32> = (0..len).map(|_| rng.random::<f32>() * 2.0 - 1.0).collect();
3684            let x64: Vec<f64> = x32.iter().map(|&v| v as f64).collect();
3685            let y64: Vec<f64> = y32.iter().map(|&v| v as f64).collect();
3686            let a32 = 0.75_f32;
3687            let a64 = 0.75_f64;
3688
3689            let mut want32 = y32.clone();
3690            axpy_scalar_f32(&mut want32, a32, &x32);
3691            let mut want64 = y64.clone();
3692            axpy_scalar_f64(&mut want64, a64, &x64);
3693
3694            // Dispatch, i.e. whatever this machine actually picks.
3695            let mut got32 = y32.clone();
3696            axpy_simd_f32(&mut got32, a32, &x32);
3697            let mut got64 = y64.clone();
3698            axpy_simd_f64(&mut got64, a64, &x64);
3699            for i in 0..len {
3700                assert_relative_eq!(got32[i], want32[i], epsilon = 1e-6, max_relative = 1e-6);
3701                assert_relative_eq!(got64[i], want64[i], epsilon = 1e-14, max_relative = 1e-14);
3702            }
3703
3704            let mut sse32 = y32.clone();
3705            axpy_sse_f32(&mut sse32, a32, &x32);
3706            let mut sse64 = y64.clone();
3707            axpy_sse_f64(&mut sse64, a64, &x64);
3708            for i in 0..len {
3709                assert_relative_eq!(sse32[i], want32[i], epsilon = 1e-6, max_relative = 1e-6);
3710                assert_relative_eq!(sse64[i], want64[i], epsilon = 1e-14, max_relative = 1e-14);
3711            }
3712
3713            #[cfg(target_arch = "x86_64")]
3714            {
3715                if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
3716                    let mut avx32 = y32.clone();
3717                    let mut avx64 = y64.clone();
3718                    unsafe {
3719                        axpy_avx2_f32(&mut avx32, a32, &x32);
3720                        axpy_avx2_f64(&mut avx64, a64, &x64);
3721                    }
3722                    for i in 0..len {
3723                        assert_relative_eq!(
3724                            avx32[i],
3725                            want32[i],
3726                            epsilon = 1e-6,
3727                            max_relative = 1e-6
3728                        );
3729                        assert_relative_eq!(
3730                            avx64[i],
3731                            want64[i],
3732                            epsilon = 1e-14,
3733                            max_relative = 1e-14
3734                        );
3735                    }
3736                }
3737
3738                if is_x86_feature_detected!("avx512f") {
3739                    let mut avx32 = y32.clone();
3740                    let mut avx64 = y64.clone();
3741                    unsafe {
3742                        axpy_avx512_f32(&mut avx32, a32, &x32);
3743                        axpy_avx512_f64(&mut avx64, a64, &x64);
3744                    }
3745                    for i in 0..len {
3746                        assert_relative_eq!(
3747                            avx32[i],
3748                            want32[i],
3749                            epsilon = 1e-6,
3750                            max_relative = 1e-6
3751                        );
3752                        assert_relative_eq!(
3753                            avx64[i],
3754                            want64[i],
3755                            epsilon = 1e-14,
3756                            max_relative = 1e-14
3757                        );
3758                    }
3759                }
3760            }
3761        }
3762    }
3763
3764    /// A zero multiplier must leave the accumulator untouched, and a unit
3765    /// multiplier must give a plain elementwise add. Both are edge cases the
3766    /// sparse kernel hits whenever an operator row carries an exact zero.
3767    #[test]
3768    fn test_axpy_trivial_multipliers() {
3769        let x = vec![1.5_f32, -2.0, 3.25, 0.5, 7.0];
3770        let y0 = vec![0.5_f32, 0.25, -1.0, 2.0, -3.5];
3771
3772        let mut y = y0.clone();
3773        axpy_simd_f32(&mut y, 0.0, &x);
3774        assert_eq!(y, y0);
3775
3776        let mut y = y0.clone();
3777        axpy_simd_f32(&mut y, 1.0, &x);
3778        let want: Vec<f32> = y0.iter().zip(x.iter()).map(|(&a, &b)| a + b).collect();
3779        assert_eq!(y, want);
3780    }
3781}