Skip to main content

tract_linalg/generic/
reduce.rs

1// Reduce<max> generic implementation
2pub mod max {
3    pub use tract_data::internal::f16;
4
5    routine_reduce_rust!(generic;
6        f32,
7        SMax4,
8        4,
9        4,
10        fn run(x: &[f32], _: ()) -> f32 {
11            debug_assert!(x.len() % Self::nr() == 0);
12            debug_assert!(x.as_ptr() as usize % Self::alignment_bytes() == 0);
13            *x.iter().max_by(|a, b| a.total_cmp(b)).unwrap()
14        },
15        op(Max)
16    );
17
18    routine_reduce_rust!(generic;
19        f16,
20        HMax8,
21        8,
22        8,
23        fn run(x: &[f16], _: ()) -> f16 {
24            debug_assert!(x.len() % Self::nr() == 0);
25            debug_assert!(x.as_ptr() as usize % Self::alignment_bytes() == 0);
26            *x.iter().max_by(|a, b| a.total_cmp(b)).unwrap()
27        },
28        op(Max)
29    );
30}
31
32// Reduce<min> generic implementation
33pub mod min {
34    pub use tract_data::internal::f16;
35
36    routine_reduce_rust!(generic;
37        f32,
38        SMin4,
39        4,
40        4,
41        fn run(x: &[f32], _: ()) -> f32 {
42            debug_assert!(x.len() % Self::nr() == 0);
43            debug_assert!(x.as_ptr() as usize % Self::alignment_bytes() == 0);
44            *x.iter().min_by(|a, b| a.total_cmp(b)).unwrap()
45        },
46        op(Min)
47    );
48}
49
50// Reduce<sum> generic implementation
51pub mod sum {
52    pub use tract_data::internal::f16;
53
54    routine_reduce_rust!(generic;
55        f32,
56        SSum4,
57        4,
58        4,
59        fn run(x: &[f32], _: ()) -> f32 {
60            debug_assert!(x.len() % Self::nr() == 0);
61            debug_assert!(x.as_ptr() as usize % Self::alignment_bytes() == 0);
62            x.iter().sum::<f32>()
63        },
64        op(Sum)
65    );
66
67    routine_reduce_rust!(generic;
68        f16,
69        HSum8,
70        8,
71        8,
72        fn run(x: &[f16], _: ()) -> f16 {
73            debug_assert!(x.len() % Self::nr() == 0);
74            debug_assert!(x.as_ptr() as usize % Self::alignment_bytes() == 0);
75            // f32 accumulator: a row long enough for the running sum to outgrow
76            // its own terms stalls in f16. The vector kernels are shielded by
77            // holding one partial per lane; this one is not.
78            f16::from_f32(x.iter().map(|v| v.to_f32()).sum::<f32>())
79        },
80        op(Sum)
81    );
82}
83
84// Softmax generic implementation
85pub mod softmax_l2 {
86
87    /// exp(x - max) with a Cody-Waite reduction and a degree-6 fit: accurate to
88    /// about 1e-7 relative while still being all FMAs so the row loop vectorizes.
89    #[inline(always)]
90    pub fn accurate_exp_f32(x: f32) -> f32 {
91        const LOG2E: f32 = 1.442_695_04;
92        const LN2_HI: f32 = 0.693_145_75;
93        const LN2_LO: f32 = 1.428_606_8e-6;
94        const MAGIC: f32 = 12_582_912.0;
95        let kf = (x * LOG2E + MAGIC) - MAGIC;
96        let rr = kf.mul_add(-LN2_LO, kf.mul_add(-LN2_HI, x));
97        let mut q = 1.383684405e-03f32;
98        q = q.mul_add(rr, 8.374815793e-03);
99        q = q.mul_add(rr, 4.166822560e-02);
100        q = q.mul_add(rr, 1.666642017e-01);
101        q = q.mul_add(rr, 4.999999208e-01);
102        q = q.mul_add(rr, 1.000000036e+00);
103        q = q.mul_add(rr, 1.000000001e+00);
104        let k = kf as i32;
105        let scale = f32::from_bits(((k + 127).clamp(1, 254) as u32) << 23);
106        // The argument is `v - max` over a row, so it is never positive; a
107        // positive value only arises from the f32::MIN lanes a short row is
108        // padded with, and below -103 exp underflows to zero. Selecting here
109        // rather than returning early keeps the loop vectorizable. Not a range
110        // check: `contains` is true-by-negation for NaN, which would return zero
111        // where a fully masked row must still reduce to NaN.
112        #[allow(clippy::manual_range_contains)]
113        if x < -103.0 || x > 0.0 { 0.0 } else { q * scale }
114    }
115
116    /// exp(x - max) over a row, returning the sum. Every lane of the load, the
117    /// polynomial and the accumulation stays four wide; auto-vectorization
118    /// leaves the integer half of the scale reconstruction scalar, which costs
119    /// most of the throughput.
120    #[cfg(target_arch = "aarch64")]
121    #[inline]
122    fn exp_sum_impl(x: &mut [f32], max: f32) -> f32 {
123        use std::arch::aarch64::*;
124        unsafe {
125            #[inline(always)]
126            unsafe fn exp_ps(x: float32x4_t) -> float32x4_t {
127                unsafe {
128                    let kf = vrndnq_f32(vmulq_f32(x, vdupq_n_f32(1.442_695_04)));
129                    let mut rr = vfmsq_f32(x, kf, vdupq_n_f32(0.693_145_75));
130                    rr = vfmsq_f32(rr, kf, vdupq_n_f32(1.428_606_8e-6));
131                    let mut q = vdupq_n_f32(8.297653546e-03);
132                    q = vfmaq_f32(vdupq_n_f32(4.191538191e-02), q, rr);
133                    q = vfmaq_f32(vdupq_n_f32(1.666757475e-01), q, rr);
134                    q = vfmaq_f32(vdupq_n_f32(4.999889485e-01), q, rr);
135                    q = vfmaq_f32(vdupq_n_f32(9.999996920e-01), q, rr);
136                    q = vfmaq_f32(vdupq_n_f32(1.000000072e+00), q, rr);
137                    let k = vcvtq_s32_f32(kf);
138                    // The biased exponent must stay a valid field: k + 127 goes
139                    // non-positive around x = -88, and shifting that in would
140                    // build -inf instead of a small number, poisoning the sum.
141                    let biased = vmaxq_s32(
142                        vminq_s32(vaddq_s32(k, vdupq_n_s32(127)), vdupq_n_s32(254)),
143                        vdupq_n_s32(1),
144                    );
145                    let scale = vreinterpretq_f32_s32(vshlq_n_s32(biased, 23));
146                    let out = vorrq_u32(
147                        vcltq_f32(x, vdupq_n_f32(-103.0)),
148                        vcgtq_f32(x, vdupq_n_f32(0.0)),
149                    );
150                    vbslq_f32(out, vdupq_n_f32(0.0), vmulq_f32(q, scale))
151                }
152            }
153            let vm = vdupq_n_f32(max);
154            let mut vsum = vdupq_n_f32(0.0);
155            let mut i = 0;
156            while i + 4 <= x.len() {
157                let y = exp_ps(vsubq_f32(vld1q_f32(x.as_ptr().add(i)), vm));
158                vst1q_f32(x.as_mut_ptr().add(i), y);
159                vsum = vaddq_f32(vsum, y);
160                i += 4;
161            }
162            let mut sum = vaddvq_f32(vsum);
163            for v in &mut x[i..] {
164                let y = accurate_exp_f32(*v - max);
165                *v = y;
166                sum += y;
167            }
168            sum
169        }
170    }
171
172    /// simd128 counterpart. wasm has no fused multiply-add outside relaxed-simd,
173    /// so the polynomial is a separate multiply and add per step; the reduction
174    /// is what matters here, since LLVM does not vectorize f32 reductions on
175    /// this target at all.
176    #[cfg(all(target_family = "wasm", target_feature = "simd128"))]
177    #[inline]
178    fn exp_sum_impl(x: &mut [f32], max: f32) -> f32 {
179        use std::arch::wasm32::*;
180        #[inline(always)]
181        fn exp_ps(x: v128) -> v128 {
182            let kf = f32x4_nearest(f32x4_mul(x, f32x4_splat(1.442_695_04)));
183            let mut rr = f32x4_sub(x, f32x4_mul(kf, f32x4_splat(0.693_145_75)));
184            rr = f32x4_sub(rr, f32x4_mul(kf, f32x4_splat(1.428_606_8e-6)));
185            let mut q = f32x4_splat(8.297653546e-03);
186            q = f32x4_add(f32x4_splat(4.191538191e-02), f32x4_mul(q, rr));
187            q = f32x4_add(f32x4_splat(1.666757475e-01), f32x4_mul(q, rr));
188            q = f32x4_add(f32x4_splat(4.999889485e-01), f32x4_mul(q, rr));
189            q = f32x4_add(f32x4_splat(9.999996920e-01), f32x4_mul(q, rr));
190            q = f32x4_add(f32x4_splat(1.000000072e+00), f32x4_mul(q, rr));
191            let k = i32x4_trunc_sat_f32x4(kf);
192            // Same guard as the scalar and NEON forms: an out-of-range biased
193            // exponent would shift in as -inf rather than a small number.
194            let biased = i32x4_max(
195                i32x4_min(i32x4_add(k, i32x4_splat(127)), i32x4_splat(254)),
196                i32x4_splat(1),
197            );
198            let scale = i32x4_shl(biased, 23);
199            let out = v128_or(f32x4_lt(x, f32x4_splat(-103.0)), f32x4_gt(x, f32x4_splat(0.0)));
200            v128_bitselect(f32x4_splat(0.0), f32x4_mul(q, scale), out)
201        }
202        let vm = f32x4_splat(max);
203        let mut vsum = f32x4_splat(0.0);
204        let mut i = 0;
205        while i + 4 <= x.len() {
206            let y = exp_ps(f32x4_sub(unsafe { v128_load(x.as_ptr().add(i) as *const v128) }, vm));
207            unsafe { v128_store(x.as_mut_ptr().add(i) as *mut v128, y) };
208            vsum = f32x4_add(vsum, y);
209            i += 4;
210        }
211        let mut sum = f32x4_extract_lane::<0>(vsum)
212            + f32x4_extract_lane::<1>(vsum)
213            + f32x4_extract_lane::<2>(vsum)
214            + f32x4_extract_lane::<3>(vsum);
215        for v in &mut x[i..] {
216            let y = accurate_exp_f32(*v - max);
217            *v = y;
218            sum += y;
219        }
220        sum
221    }
222
223    #[cfg(not(any(
224        target_arch = "aarch64",
225        all(target_family = "wasm", target_feature = "simd128")
226    )))]
227    #[inline]
228    fn exp_sum_impl(x: &mut [f32], max: f32) -> f32 {
229        let mut acc = [0f32; 4];
230        let mut it = x.chunks_exact_mut(4);
231        for c in &mut it {
232            for (j, v) in c.iter_mut().enumerate() {
233                let y = accurate_exp_f32(*v - max);
234                *v = y;
235                acc[j] += y;
236            }
237        }
238        let mut sum = (acc[0] + acc[1]) + (acc[2] + acc[3]);
239        for v in it.into_remainder().iter_mut() {
240            let y = accurate_exp_f32(*v - max);
241            *v = y;
242            sum += y;
243        }
244        sum
245    }
246
247    routine_map_reduce_rust!(generic;
248        f32,
249        SSoftMaxL2Accurate,
250        4,
251        4,
252        fn run(x: &mut [f32], max: f32) -> f32 {
253            debug_assert!(x.len() % Self::nr() == 0);
254            debug_assert!(x.as_ptr() as usize % Self::alignment_bytes() == 0);
255            exp_sum_impl(x, max)
256        },
257        op(Softmax2)
258    );
259}
260
261#[cfg(test)]
262mod f16_accumulators {
263    use super::*;
264    use crate::frame::reduce::ReduceKer;
265    use tract_data::internal::f16;
266
267    /// The returned row sum must stay close to the same sum taken in f32. A row
268    /// long enough for the running total to outgrow its own terms is the case an
269    /// f16 accumulator silently drops.
270    #[test]
271    fn plain_sum_keeps_long_rows() {
272        for len in [1024usize, 4096, 8192] {
273            let row: Vec<f16> = vec![f16::from_f32(1.0); len];
274            let got = sum::HSum8::red().run(&row).unwrap().to_f32();
275            let err = (got - len as f32).abs() / len as f32;
276            assert!(err < 0.01, "len {len}: summed to {got}, rel {err}");
277        }
278    }
279}