Skip to main content

polars_compute/
float_sum.rs

1use std::ops::{Add, IndexMut};
2#[cfg(feature = "simd")]
3use std::simd::{prelude::*, *};
4
5use arrow::array::{Array, PrimitiveArray};
6use arrow::bitmap::Bitmap;
7use arrow::bitmap::bitmask::BitMask;
8use arrow::types::NativeType;
9use num_traits::{AsPrimitive, Float};
10#[cfg(feature = "simd")]
11use polars_utils::float16::pf16;
12
13const STRIPE: usize = 16;
14const PAIRWISE_RECURSION_LIMIT: usize = 128;
15
16// We want to be generic over both integers and floats, requiring this helper trait.
17#[cfg(feature = "simd")]
18pub trait SimdCastGeneric<const N: usize> {
19    fn cast_generic<U: SimdCast>(self) -> Simd<U, N>;
20}
21
22macro_rules! impl_cast_custom {
23    ($_type:ty) => {
24        #[cfg(feature = "simd")]
25        impl<const N: usize> SimdCastGeneric<N> for Simd<$_type, N> {
26            fn cast_generic<U: SimdCast>(self) -> Simd<U, N> {
27                self.cast::<U>()
28            }
29        }
30    };
31}
32
33impl_cast_custom!(u8);
34impl_cast_custom!(u16);
35impl_cast_custom!(u32);
36impl_cast_custom!(u64);
37impl_cast_custom!(i8);
38impl_cast_custom!(i16);
39impl_cast_custom!(i32);
40impl_cast_custom!(i64);
41impl_cast_custom!(f32);
42impl_cast_custom!(f64);
43
44fn vector_horizontal_sum<V, T>(mut v: V) -> T
45where
46    V: IndexMut<usize, Output = T>,
47    T: Add<T, Output = T> + Sized + Copy,
48{
49    // We have to be careful about this reduction, floating
50    // point math is NOT associative so we have to write this
51    // in a form that maps to good shuffle instructions.
52    // We fold the vector onto itself, halved, until we are down to
53    // four elements which we add in a shuffle-friendly way.
54    let mut width = STRIPE;
55    while width > 4 {
56        for j in 0..width / 2 {
57            v[j] = v[j] + v[width / 2 + j];
58        }
59        width /= 2;
60    }
61
62    (v[0] + v[2]) + (v[1] + v[3])
63}
64
65// As a trait to not proliferate SIMD bounds.
66pub trait SumBlock<F> {
67    fn sum_block_vectorized(&self) -> F;
68    fn sum_block_vectorized_with_mask(&self, mask: BitMask<'_>) -> F;
69}
70
71#[cfg(feature = "simd")]
72impl<T, F> SumBlock<F> for [T; PAIRWISE_RECURSION_LIMIT]
73where
74    T: SimdElement,
75    F: SimdElement + SimdCast + Add<Output = F> + Default,
76    Simd<T, STRIPE>: SimdCastGeneric<STRIPE>,
77    Simd<F, STRIPE>: std::iter::Sum,
78{
79    fn sum_block_vectorized(&self) -> F {
80        let vsum = self
81            .chunks_exact(STRIPE)
82            .map(|a| Simd::<T, STRIPE>::from_slice(a).cast_generic::<F>())
83            .sum::<Simd<F, STRIPE>>();
84        vector_horizontal_sum(vsum)
85    }
86
87    fn sum_block_vectorized_with_mask(&self, mask: BitMask<'_>) -> F {
88        let zero = Simd::default();
89        let vsum = self
90            .chunks_exact(STRIPE)
91            .enumerate()
92            .map(|(i, a)| {
93                let m: Mask<T::Mask, STRIPE> = mask.get_simd(i * STRIPE);
94                m.select(Simd::from_slice(a).cast_generic::<F>(), zero)
95            })
96            .sum::<Simd<F, STRIPE>>();
97        vector_horizontal_sum(vsum)
98    }
99}
100
101#[cfg(feature = "simd")]
102impl<F> SumBlock<F> for [i128; PAIRWISE_RECURSION_LIMIT]
103where
104    i128: AsPrimitive<F>,
105    F: Float + std::iter::Sum + 'static,
106{
107    fn sum_block_vectorized(&self) -> F {
108        self.iter().map(|x| x.as_()).sum()
109    }
110
111    fn sum_block_vectorized_with_mask(&self, mask: BitMask<'_>) -> F {
112        self.iter()
113            .enumerate()
114            .map(|(idx, x)| if mask.get(idx) { x.as_() } else { F::zero() })
115            .sum()
116    }
117}
118
119#[cfg(feature = "simd")]
120impl<F> SumBlock<F> for [u128; PAIRWISE_RECURSION_LIMIT]
121where
122    u128: AsPrimitive<F>,
123    F: Float + std::iter::Sum + 'static,
124{
125    fn sum_block_vectorized(&self) -> F {
126        self.iter().map(|x| x.as_()).sum()
127    }
128
129    fn sum_block_vectorized_with_mask(&self, mask: BitMask<'_>) -> F {
130        self.iter()
131            .enumerate()
132            .map(|(idx, x)| if mask.get(idx) { x.as_() } else { F::zero() })
133            .sum()
134    }
135}
136
137#[cfg(feature = "simd")]
138impl<F> SumBlock<F> for [pf16; PAIRWISE_RECURSION_LIMIT]
139where
140    pf16: AsPrimitive<F>,
141    F: Float + std::iter::Sum + 'static,
142{
143    fn sum_block_vectorized(&self) -> F {
144        self.iter().map(|x| x.as_()).sum()
145    }
146
147    fn sum_block_vectorized_with_mask(&self, mask: BitMask<'_>) -> F {
148        self.iter()
149            .enumerate()
150            .map(|(idx, x)| if mask.get(idx) { x.as_() } else { F::zero() })
151            .sum()
152    }
153}
154
155#[cfg(not(feature = "simd"))]
156impl<T, F> SumBlock<F> for [T; PAIRWISE_RECURSION_LIMIT]
157where
158    T: AsPrimitive<F> + 'static,
159    F: Default + Add<Output = F> + Copy + 'static,
160{
161    fn sum_block_vectorized(&self) -> F {
162        let mut vsum = [F::default(); STRIPE];
163        for chunk in self.chunks_exact(STRIPE) {
164            for j in 0..STRIPE {
165                vsum[j] = vsum[j] + chunk[j].as_();
166            }
167        }
168        vector_horizontal_sum(vsum)
169    }
170
171    fn sum_block_vectorized_with_mask(&self, mask: BitMask<'_>) -> F {
172        let mut vsum = [F::default(); STRIPE];
173        for (i, chunk) in self.chunks_exact(STRIPE).enumerate() {
174            for j in 0..STRIPE {
175                // Unconditional add with select for better branch-free opts.
176                let addend = if mask.get(i * STRIPE + j) {
177                    chunk[j].as_()
178                } else {
179                    F::default()
180                };
181                vsum[j] = vsum[j] + addend;
182            }
183        }
184        vector_horizontal_sum(vsum)
185    }
186}
187
188/// Invariant: f.len() % PAIRWISE_RECURSION_LIMIT == 0 and f.len() > 0.
189unsafe fn pairwise_sum<F, T>(f: &[T]) -> F
190where
191    [T; PAIRWISE_RECURSION_LIMIT]: SumBlock<F>,
192    F: Add<Output = F>,
193{
194    debug_assert!(!f.is_empty() && f.len().is_multiple_of(PAIRWISE_RECURSION_LIMIT));
195
196    let block: Option<&[T; PAIRWISE_RECURSION_LIMIT]> = f.try_into().ok();
197    if let Some(block) = block {
198        return block.sum_block_vectorized();
199    }
200
201    // SAFETY: we maintain the invariant. `try_into` array of len PAIRWISE_RECURSION_LIMIT
202    // failed so we know f.len() >= 2*PAIRWISE_RECURSION_LIMIT, and thus blocks >= 2.
203    // This means 0 < left_len < f.len() and left_len is divisible by PAIRWISE_RECURSION_LIMIT,
204    // maintaining the invariant for both recursive calls.
205    unsafe {
206        let blocks = f.len() / PAIRWISE_RECURSION_LIMIT;
207        let left_len = (blocks / 2) * PAIRWISE_RECURSION_LIMIT;
208        let (left, right) = (f.get_unchecked(..left_len), f.get_unchecked(left_len..));
209        pairwise_sum(left) + pairwise_sum(right)
210    }
211}
212
213/// Invariant: f.len() % PAIRWISE_RECURSION_LIMIT == 0 and f.len() > 0.
214/// Also, f.len() == mask.len().
215unsafe fn pairwise_sum_with_mask<F, T>(f: &[T], mask: BitMask<'_>) -> F
216where
217    [T; PAIRWISE_RECURSION_LIMIT]: SumBlock<F>,
218    F: Add<Output = F>,
219{
220    debug_assert!(!f.is_empty() && f.len().is_multiple_of(PAIRWISE_RECURSION_LIMIT));
221    debug_assert!(f.len() == mask.len());
222
223    let block: Option<&[T; PAIRWISE_RECURSION_LIMIT]> = f.try_into().ok();
224    if let Some(block) = block {
225        return block.sum_block_vectorized_with_mask(mask);
226    }
227
228    // SAFETY: see pairwise_sum.
229    unsafe {
230        let blocks = f.len() / PAIRWISE_RECURSION_LIMIT;
231        let left_len = (blocks / 2) * PAIRWISE_RECURSION_LIMIT;
232        let (left, right) = (f.get_unchecked(..left_len), f.get_unchecked(left_len..));
233        let (left_mask, right_mask) = mask.split_at_unchecked(left_len);
234        pairwise_sum_with_mask(left, left_mask) + pairwise_sum_with_mask(right, right_mask)
235    }
236}
237
238pub trait FloatSum<F>: Sized {
239    fn sum(f: &[Self]) -> F;
240    fn sum_with_validity(f: &[Self], validity: &Bitmap) -> F;
241}
242
243impl<T, F> FloatSum<F> for T
244where
245    F: Float + std::iter::Sum + 'static,
246    T: AsPrimitive<F>,
247    [T; PAIRWISE_RECURSION_LIMIT]: SumBlock<F>,
248{
249    fn sum(f: &[Self]) -> F {
250        let remainder = f.len() % PAIRWISE_RECURSION_LIMIT;
251        let (rest, main) = f.split_at(remainder);
252        let mainsum = if f.len() > remainder {
253            unsafe { pairwise_sum(main) }
254        } else {
255            F::zero()
256        };
257        // TODO: faster remainder.
258        let restsum: F = rest.iter().map(|x| x.as_()).sum();
259        mainsum + restsum
260    }
261
262    fn sum_with_validity(f: &[Self], validity: &Bitmap) -> F {
263        let mask = BitMask::from_bitmap(validity);
264        assert!(f.len() == mask.len());
265
266        let remainder = f.len() % PAIRWISE_RECURSION_LIMIT;
267        let (rest, main) = f.split_at(remainder);
268        let (rest_mask, main_mask) = mask.split_at(remainder);
269        let mainsum = if f.len() > remainder {
270            unsafe { pairwise_sum_with_mask(main, main_mask) }
271        } else {
272            F::zero()
273        };
274        // TODO: faster remainder.
275        let restsum: F = rest
276            .iter()
277            .enumerate()
278            .map(|(i, x)| {
279                // No filter but rather select of 0.0 for cmov opt.
280                if rest_mask.get(i) { x.as_() } else { F::zero() }
281            })
282            .sum();
283        mainsum + restsum
284    }
285}
286
287pub fn sum_arr_as_f32<T>(arr: &PrimitiveArray<T>) -> f32
288where
289    T: NativeType + FloatSum<f32>,
290{
291    let validity = arr.validity().filter(|_| arr.null_count() > 0);
292    if let Some(mask) = validity {
293        FloatSum::sum_with_validity(arr.values(), mask)
294    } else {
295        FloatSum::sum(arr.values())
296    }
297}
298
299pub fn sum_arr_as_f64<T>(arr: &PrimitiveArray<T>) -> f64
300where
301    T: NativeType + FloatSum<f64>,
302{
303    let validity = arr.validity().filter(|_| arr.null_count() > 0);
304    if let Some(mask) = validity {
305        FloatSum::sum_with_validity(arr.values(), mask)
306    } else {
307        FloatSum::sum(arr.values())
308    }
309}