Skip to main content

oxiblas_core/scalar/
batch.rs

1//! Batch operations, SIMD compatibility, classification, and summation algorithms.
2
3use num_complex::{Complex32, Complex64};
4
5use super::traits::Scalar;
6
7#[cfg(feature = "f16")]
8use half::f16;
9
10#[cfg(feature = "f128")]
11use super::extended::QuadFloat;
12
13// =============================================================================
14// Scalar trait specialization for performance
15// =============================================================================
16
17/// Marker trait for types with hardware FMA (fused multiply-add) support.
18///
19/// Types implementing this trait have efficient hardware FMA instructions,
20/// enabling optimized implementations of algorithms like dot products and
21/// matrix multiplications.
22pub trait HasFastFma: Scalar {}
23
24impl HasFastFma for f32 {}
25impl HasFastFma for f64 {}
26impl HasFastFma for Complex32 {}
27impl HasFastFma for Complex64 {}
28
29/// Marker trait for types that can be efficiently vectorized with SIMD.
30///
31/// This trait indicates that the type has a natural mapping to SIMD registers
32/// and operations.
33pub trait SimdCompatible: Scalar {
34    /// The preferred SIMD width (number of elements) for this type.
35    const SIMD_WIDTH: usize;
36
37    /// Returns true if SIMD operations are beneficial for the given length.
38    #[inline]
39    fn use_simd_for(len: usize) -> bool {
40        len >= Self::SIMD_WIDTH * 2
41    }
42}
43
44impl SimdCompatible for f32 {
45    #[cfg(target_arch = "x86_64")]
46    const SIMD_WIDTH: usize = 8; // AVX2: 256-bit / 32-bit = 8
47
48    #[cfg(target_arch = "aarch64")]
49    const SIMD_WIDTH: usize = 4; // NEON: 128-bit / 32-bit = 4
50
51    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
52    const SIMD_WIDTH: usize = 4;
53}
54
55impl SimdCompatible for f64 {
56    #[cfg(target_arch = "x86_64")]
57    const SIMD_WIDTH: usize = 4; // AVX2: 256-bit / 64-bit = 4
58
59    #[cfg(target_arch = "aarch64")]
60    const SIMD_WIDTH: usize = 2; // NEON: 128-bit / 64-bit = 2
61
62    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
63    const SIMD_WIDTH: usize = 2;
64}
65
66impl SimdCompatible for Complex32 {
67    // Complex types have half the SIMD width due to doubled storage
68    #[cfg(target_arch = "x86_64")]
69    const SIMD_WIDTH: usize = 4;
70
71    #[cfg(target_arch = "aarch64")]
72    const SIMD_WIDTH: usize = 2;
73
74    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
75    const SIMD_WIDTH: usize = 2;
76}
77
78impl SimdCompatible for Complex64 {
79    #[cfg(target_arch = "x86_64")]
80    const SIMD_WIDTH: usize = 2;
81
82    #[cfg(target_arch = "aarch64")]
83    const SIMD_WIDTH: usize = 1;
84
85    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
86    const SIMD_WIDTH: usize = 1;
87}
88
89/// Batch operations on scalar arrays for performance-critical code.
90///
91/// This trait provides straightforward serial-loop implementations of
92/// common operations on contiguous arrays of scalars. The loops are written
93/// so that LLVM's auto-vectorizer can pack them into SIMD instructions where
94/// the target and operation allow it, but there are no hand-written SIMD
95/// intrinsics behind these methods — for explicit, architecture-specific
96/// SIMD kernels see the `oxiblas_core::simd` module instead.
97pub trait ScalarBatch: Scalar + SimdCompatible {
98    /// Computes the dot product of two slices.
99    ///
100    /// # Panics
101    /// In debug builds, panics (via `debug_assert_eq!`) if `x` and `y`
102    /// differ in length. In release builds this check is compiled out, so
103    /// a length mismatch will instead cause an out-of-bounds index panic
104    /// when the shorter slice is exhausted. No unsafe code is involved.
105    fn dot_batch(x: &[Self], y: &[Self]) -> Self;
106
107    /// Computes the sum of all elements.
108    fn sum_batch(x: &[Self]) -> Self;
109
110    /// Computes the sum of absolute values (L1 norm).
111    fn asum_batch(x: &[Self]) -> Self::Real;
112
113    /// Finds the index of the element with maximum absolute value.
114    fn iamax_batch(x: &[Self]) -> usize;
115
116    /// Scales a vector: x = alpha * x
117    fn scale_batch(alpha: Self, x: &mut [Self]);
118
119    /// AXPY operation: y = alpha * x + y
120    fn axpy_batch(alpha: Self, x: &[Self], y: &mut [Self]);
121
122    /// Fused multiply-add on arrays: `z[i] = a[i] * b[i] + c[i]`
123    fn fma_batch(a: &[Self], b: &[Self], c: &[Self], out: &mut [Self]);
124}
125
126impl ScalarBatch for f32 {
127    #[inline]
128    fn dot_batch(x: &[Self], y: &[Self]) -> Self {
129        debug_assert_eq!(x.len(), y.len());
130        let mut sum = 0.0f32;
131        for i in 0..x.len() {
132            sum = x[i].mul_add(y[i], sum);
133        }
134        sum
135    }
136
137    #[inline]
138    fn sum_batch(x: &[Self]) -> Self {
139        x.iter().copied().sum()
140    }
141
142    #[inline]
143    fn asum_batch(x: &[Self]) -> Self::Real {
144        x.iter().map(|&v| v.abs()).sum()
145    }
146
147    #[inline]
148    fn iamax_batch(x: &[Self]) -> usize {
149        // Mirrors reference BLAS ISAMAX: first index wins on ties, and a
150        // strict `>` comparison means NaN never displaces the running
151        // maximum (NaN comparisons are always false under IEEE-754).
152        let Some(mut max_val) = x.first().map(|v| v.abs()) else {
153            return 0;
154        };
155        let mut max_idx = 0;
156        for (i, xi) in x.iter().enumerate().skip(1) {
157            let val = xi.abs();
158            if val > max_val {
159                max_val = val;
160                max_idx = i;
161            }
162        }
163        max_idx
164    }
165
166    #[inline]
167    fn scale_batch(alpha: Self, x: &mut [Self]) {
168        for xi in x.iter_mut() {
169            *xi *= alpha;
170        }
171    }
172
173    #[inline]
174    fn axpy_batch(alpha: Self, x: &[Self], y: &mut [Self]) {
175        debug_assert_eq!(x.len(), y.len());
176        for i in 0..x.len() {
177            y[i] = alpha.mul_add(x[i], y[i]);
178        }
179    }
180
181    #[inline]
182    fn fma_batch(a: &[Self], b: &[Self], c: &[Self], out: &mut [Self]) {
183        debug_assert_eq!(a.len(), b.len());
184        debug_assert_eq!(a.len(), c.len());
185        debug_assert_eq!(a.len(), out.len());
186        for i in 0..a.len() {
187            out[i] = a[i].mul_add(b[i], c[i]);
188        }
189    }
190}
191
192impl ScalarBatch for f64 {
193    #[inline]
194    fn dot_batch(x: &[Self], y: &[Self]) -> Self {
195        debug_assert_eq!(x.len(), y.len());
196        let mut sum = 0.0f64;
197        for i in 0..x.len() {
198            sum = x[i].mul_add(y[i], sum);
199        }
200        sum
201    }
202
203    #[inline]
204    fn sum_batch(x: &[Self]) -> Self {
205        x.iter().copied().sum()
206    }
207
208    #[inline]
209    fn asum_batch(x: &[Self]) -> Self::Real {
210        x.iter().map(|&v| v.abs()).sum()
211    }
212
213    #[inline]
214    fn iamax_batch(x: &[Self]) -> usize {
215        // Mirrors reference BLAS IDAMAX: first index wins on ties, and a
216        // strict `>` comparison means NaN never displaces the running
217        // maximum (NaN comparisons are always false under IEEE-754).
218        let Some(mut max_val) = x.first().map(|v| v.abs()) else {
219            return 0;
220        };
221        let mut max_idx = 0;
222        for (i, xi) in x.iter().enumerate().skip(1) {
223            let val = xi.abs();
224            if val > max_val {
225                max_val = val;
226                max_idx = i;
227            }
228        }
229        max_idx
230    }
231
232    #[inline]
233    fn scale_batch(alpha: Self, x: &mut [Self]) {
234        for xi in x.iter_mut() {
235            *xi *= alpha;
236        }
237    }
238
239    #[inline]
240    fn axpy_batch(alpha: Self, x: &[Self], y: &mut [Self]) {
241        debug_assert_eq!(x.len(), y.len());
242        for i in 0..x.len() {
243            y[i] = alpha.mul_add(x[i], y[i]);
244        }
245    }
246
247    #[inline]
248    fn fma_batch(a: &[Self], b: &[Self], c: &[Self], out: &mut [Self]) {
249        debug_assert_eq!(a.len(), b.len());
250        debug_assert_eq!(a.len(), c.len());
251        debug_assert_eq!(a.len(), out.len());
252        for i in 0..a.len() {
253            out[i] = a[i].mul_add(b[i], c[i]);
254        }
255    }
256}
257
258impl ScalarBatch for Complex32 {
259    #[inline]
260    fn dot_batch(x: &[Self], y: &[Self]) -> Self {
261        debug_assert_eq!(x.len(), y.len());
262        let mut sum = Complex32::new(0.0, 0.0);
263        for i in 0..x.len() {
264            sum += x[i] * y[i];
265        }
266        sum
267    }
268
269    #[inline]
270    fn sum_batch(x: &[Self]) -> Self {
271        x.iter().copied().sum()
272    }
273
274    #[inline]
275    fn asum_batch(x: &[Self]) -> Self::Real {
276        x.iter().map(|z| z.re.abs() + z.im.abs()).sum()
277    }
278
279    #[inline]
280    fn iamax_batch(x: &[Self]) -> usize {
281        // Mirrors reference BLAS ICAMAX/IZAMAX: magnitude is approximated by
282        // |re| + |im| (CABS1), first index wins on ties, and a strict `>`
283        // comparison means NaN never displaces the running maximum (NaN
284        // comparisons are always false under IEEE-754).
285        let Some(mut max_val) = x.first().map(|z| z.re.abs() + z.im.abs()) else {
286            return 0;
287        };
288        let mut max_idx = 0;
289        for (i, z) in x.iter().enumerate().skip(1) {
290            let val = z.re.abs() + z.im.abs();
291            if val > max_val {
292                max_val = val;
293                max_idx = i;
294            }
295        }
296        max_idx
297    }
298
299    #[inline]
300    fn scale_batch(alpha: Self, x: &mut [Self]) {
301        for xi in x.iter_mut() {
302            *xi *= alpha;
303        }
304    }
305
306    #[inline]
307    fn axpy_batch(alpha: Self, x: &[Self], y: &mut [Self]) {
308        debug_assert_eq!(x.len(), y.len());
309        for i in 0..x.len() {
310            y[i] += alpha * x[i];
311        }
312    }
313
314    #[inline]
315    fn fma_batch(a: &[Self], b: &[Self], c: &[Self], out: &mut [Self]) {
316        debug_assert_eq!(a.len(), b.len());
317        debug_assert_eq!(a.len(), c.len());
318        debug_assert_eq!(a.len(), out.len());
319        for i in 0..a.len() {
320            out[i] = a[i] * b[i] + c[i];
321        }
322    }
323}
324
325impl ScalarBatch for Complex64 {
326    #[inline]
327    fn dot_batch(x: &[Self], y: &[Self]) -> Self {
328        debug_assert_eq!(x.len(), y.len());
329        let mut sum = Complex64::new(0.0, 0.0);
330        for i in 0..x.len() {
331            sum += x[i] * y[i];
332        }
333        sum
334    }
335
336    #[inline]
337    fn sum_batch(x: &[Self]) -> Self {
338        x.iter().copied().sum()
339    }
340
341    #[inline]
342    fn asum_batch(x: &[Self]) -> Self::Real {
343        x.iter().map(|z| z.re.abs() + z.im.abs()).sum()
344    }
345
346    #[inline]
347    fn iamax_batch(x: &[Self]) -> usize {
348        // Mirrors reference BLAS ICAMAX/IZAMAX: magnitude is approximated by
349        // |re| + |im| (CABS1), first index wins on ties, and a strict `>`
350        // comparison means NaN never displaces the running maximum (NaN
351        // comparisons are always false under IEEE-754).
352        let Some(mut max_val) = x.first().map(|z| z.re.abs() + z.im.abs()) else {
353            return 0;
354        };
355        let mut max_idx = 0;
356        for (i, z) in x.iter().enumerate().skip(1) {
357            let val = z.re.abs() + z.im.abs();
358            if val > max_val {
359                max_val = val;
360                max_idx = i;
361            }
362        }
363        max_idx
364    }
365
366    #[inline]
367    fn scale_batch(alpha: Self, x: &mut [Self]) {
368        for xi in x.iter_mut() {
369            *xi *= alpha;
370        }
371    }
372
373    #[inline]
374    fn axpy_batch(alpha: Self, x: &[Self], y: &mut [Self]) {
375        debug_assert_eq!(x.len(), y.len());
376        for i in 0..x.len() {
377            y[i] += alpha * x[i];
378        }
379    }
380
381    #[inline]
382    fn fma_batch(a: &[Self], b: &[Self], c: &[Self], out: &mut [Self]) {
383        debug_assert_eq!(a.len(), b.len());
384        debug_assert_eq!(a.len(), c.len());
385        debug_assert_eq!(a.len(), out.len());
386        for i in 0..a.len() {
387            out[i] = a[i] * b[i] + c[i];
388        }
389    }
390}
391
392/// Type-level scalar classification for compile-time dispatch.
393///
394/// This enum enables algorithms to specialize at compile time based on
395/// the scalar type's properties.
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum ScalarClass {
398    /// Single-precision real (f32)
399    RealF32,
400    /// Double-precision real (f64)
401    RealF64,
402    /// Single-precision complex
403    ComplexF32,
404    /// Double-precision complex
405    ComplexF64,
406    /// Half-precision real (f16)
407    RealF16,
408    /// Quad-precision real (f128)
409    RealF128,
410    /// Unknown/other type
411    Other,
412}
413
414/// Trait for compile-time scalar classification.
415pub trait ScalarClassify: Scalar {
416    /// The compile-time class of this scalar type.
417    const CLASS: ScalarClass;
418
419    /// Returns the precision level (1 = lowest, 4 = highest).
420    const PRECISION_LEVEL: u8;
421
422    /// Returns the storage size in bytes.
423    const STORAGE_BYTES: usize = core::mem::size_of::<Self>();
424}
425
426impl ScalarClassify for f32 {
427    const CLASS: ScalarClass = ScalarClass::RealF32;
428    const PRECISION_LEVEL: u8 = 2;
429}
430
431impl ScalarClassify for f64 {
432    const CLASS: ScalarClass = ScalarClass::RealF64;
433    const PRECISION_LEVEL: u8 = 3;
434}
435
436impl ScalarClassify for Complex32 {
437    const CLASS: ScalarClass = ScalarClass::ComplexF32;
438    const PRECISION_LEVEL: u8 = 2;
439}
440
441impl ScalarClassify for Complex64 {
442    const CLASS: ScalarClass = ScalarClass::ComplexF64;
443    const PRECISION_LEVEL: u8 = 3;
444}
445
446#[cfg(feature = "f16")]
447impl ScalarClassify for f16 {
448    const CLASS: ScalarClass = ScalarClass::RealF16;
449    const PRECISION_LEVEL: u8 = 1;
450}
451
452#[cfg(feature = "f128")]
453impl ScalarClassify for QuadFloat {
454    const CLASS: ScalarClass = ScalarClass::RealF128;
455    const PRECISION_LEVEL: u8 = 4;
456}
457
458/// Unrolling hints for vectorized loops.
459///
460/// These constants help the compiler make better unrolling decisions
461/// for different scalar types.
462pub trait UnrollHints: Scalar {
463    /// Recommended unroll factor for tight loops.
464    const UNROLL_FACTOR: usize;
465
466    /// Recommended chunk size for blocked algorithms.
467    const BLOCK_SIZE: usize;
468
469    /// Whether to prefer streaming stores (for large writes).
470    const PREFER_STREAMING: bool;
471}
472
473impl UnrollHints for f32 {
474    const UNROLL_FACTOR: usize = 8;
475    const BLOCK_SIZE: usize = 64;
476    const PREFER_STREAMING: bool = true;
477}
478
479impl UnrollHints for f64 {
480    const UNROLL_FACTOR: usize = 4;
481    const BLOCK_SIZE: usize = 32;
482    const PREFER_STREAMING: bool = true;
483}
484
485impl UnrollHints for Complex32 {
486    const UNROLL_FACTOR: usize = 4;
487    const BLOCK_SIZE: usize = 32;
488    const PREFER_STREAMING: bool = true;
489}
490
491impl UnrollHints for Complex64 {
492    const UNROLL_FACTOR: usize = 2;
493    const BLOCK_SIZE: usize = 16;
494    const PREFER_STREAMING: bool = true;
495}
496
497/// Extended precision accumulation support.
498///
499/// For algorithms requiring higher precision during intermediate calculations,
500/// this trait provides access to an extended precision accumulator type.
501pub trait ExtendedPrecision: Scalar {
502    /// The type used for extended precision accumulation.
503    type Accumulator: Scalar;
504
505    /// Converts a value to the accumulator type.
506    fn to_accumulator(self) -> Self::Accumulator;
507
508    /// Converts from the accumulator type back to this type.
509    fn from_accumulator(acc: Self::Accumulator) -> Self;
510}
511
512impl ExtendedPrecision for f32 {
513    type Accumulator = f64;
514
515    #[inline]
516    fn to_accumulator(self) -> f64 {
517        self as f64
518    }
519
520    #[inline]
521    fn from_accumulator(acc: f64) -> f32 {
522        acc as f32
523    }
524}
525
526/// With the `f128` feature enabled, f64 gets genuine extended-precision
527/// accumulation via the crate's double-double `QuadFloat` type (~106 bits
528/// of mantissa versus f64's 53), so intermediate sums/products retain
529/// precision well beyond what a plain `f64` accumulator could preserve.
530#[cfg(feature = "f128")]
531impl ExtendedPrecision for f64 {
532    type Accumulator = QuadFloat;
533
534    #[inline]
535    fn to_accumulator(self) -> QuadFloat {
536        QuadFloat::from(self)
537    }
538
539    #[inline]
540    fn from_accumulator(acc: QuadFloat) -> f64 {
541        // Round the double-double value back to the nearest f64: adding the
542        // low limb into the high limb performs correctly-rounded
543        // reconstruction for a normalized double-double pair.
544        let tf = acc.inner();
545        tf.hi() + tf.lo()
546    }
547}
548
549/// Without the `f128` feature, no higher-than-`f64` scalar type exists in
550/// this build, so accumulation deliberately stays at `f64`. This is a
551/// documented trade-off, not an oversight: genuine extended accumulation
552/// (via `QuadFloat`) costs roughly 2x the arithmetic per accumulate step,
553/// which is not worth paying unconditionally for every `f64` caller.
554/// Enable the `f128` feature to opt into true double-double accumulation
555/// for `f64` inputs.
556#[cfg(not(feature = "f128"))]
557impl ExtendedPrecision for f64 {
558    type Accumulator = f64;
559
560    #[inline]
561    fn to_accumulator(self) -> f64 {
562        self
563    }
564
565    #[inline]
566    fn from_accumulator(acc: f64) -> f64 {
567        acc
568    }
569}
570
571impl ExtendedPrecision for Complex32 {
572    type Accumulator = Complex64;
573
574    #[inline]
575    fn to_accumulator(self) -> Complex64 {
576        Complex64::new(self.re as f64, self.im as f64)
577    }
578
579    #[inline]
580    fn from_accumulator(acc: Complex64) -> Complex32 {
581        Complex32::new(acc.re as f32, acc.im as f32)
582    }
583}
584
585impl ExtendedPrecision for Complex64 {
586    type Accumulator = Complex64;
587
588    #[inline]
589    fn to_accumulator(self) -> Complex64 {
590        self
591    }
592
593    #[inline]
594    fn from_accumulator(acc: Complex64) -> Complex64 {
595        acc
596    }
597}
598
599// =============================================================================
600// Summation algorithms
601// =============================================================================
602
603/// Kahan summation for improved accuracy.
604///
605/// Uses compensated summation to reduce floating-point errors.
606#[derive(Debug, Clone, Copy)]
607pub struct KahanSum<T: Scalar> {
608    sum: T,
609    compensation: T,
610}
611
612impl<T: Scalar> Default for KahanSum<T> {
613    fn default() -> Self {
614        Self::new()
615    }
616}
617
618impl<T: Scalar> KahanSum<T> {
619    /// Creates a new Kahan sum accumulator initialized to zero.
620    #[inline]
621    pub fn new() -> Self {
622        Self {
623            sum: T::zero(),
624            compensation: T::zero(),
625        }
626    }
627
628    /// Adds a value to the sum with compensation.
629    #[inline]
630    pub fn add(&mut self, value: T) {
631        let y = value - self.compensation;
632        let t = self.sum + y;
633        self.compensation = (t - self.sum) - y;
634        self.sum = t;
635    }
636
637    /// Returns the current sum.
638    #[inline]
639    pub fn sum(self) -> T {
640        self.sum
641    }
642}
643
644/// Pairwise summation for reduced error accumulation.
645///
646/// Recursively splits the array and sums pairs, reducing error from O(n) to O(log n).
647#[inline]
648pub fn pairwise_sum<T: Scalar>(values: &[T]) -> T {
649    const THRESHOLD: usize = 32;
650
651    if values.is_empty() {
652        return T::zero();
653    }
654    if values.len() <= THRESHOLD {
655        return values.iter().copied().fold(T::zero(), |acc, x| acc + x);
656    }
657
658    let mid = values.len() / 2;
659    pairwise_sum(&values[..mid]) + pairwise_sum(&values[mid..])
660}
661
662/// Kahan-Babuska-Klein summation (improved compensated summation).
663///
664/// Provides even better error bounds than standard Kahan summation.
665#[derive(Debug, Clone, Copy)]
666pub struct KBKSum<T: Scalar> {
667    sum: T,
668    cs: T,
669    ccs: T,
670}
671
672impl<T: Scalar> Default for KBKSum<T> {
673    fn default() -> Self {
674        Self::new()
675    }
676}
677
678impl<T: Scalar> KBKSum<T> {
679    /// Creates a new KBK sum accumulator.
680    #[inline]
681    pub fn new() -> Self {
682        Self {
683            sum: T::zero(),
684            cs: T::zero(),
685            ccs: T::zero(),
686        }
687    }
688
689    /// Adds a value with double compensation.
690    #[inline]
691    pub fn add(&mut self, value: T) {
692        let t = self.sum + value;
693        let c = if Scalar::abs(self.sum) >= Scalar::abs(value) {
694            (self.sum - t) + value
695        } else {
696            (value - t) + self.sum
697        };
698        self.sum = t;
699
700        let t2 = self.cs + c;
701        let cc = if Scalar::abs(self.cs) >= Scalar::abs(c) {
702            (self.cs - t2) + c
703        } else {
704            (c - t2) + self.cs
705        };
706        self.cs = t2;
707        self.ccs += cc;
708    }
709
710    /// Returns the compensated sum.
711    #[inline]
712    pub fn sum(self) -> T {
713        self.sum + self.cs + self.ccs
714    }
715}