Skip to main content

rten_vecmath/
softmax.rs

1use std::mem::MaybeUninit;
2
3use rten_simd::functional::{simd_apply, simd_map};
4use rten_simd::ops::{BitOps, FloatOps, NumOps};
5use rten_simd::span::SrcDest;
6use rten_simd::{Isa, Simd, SimdIterable, SimdOp, SimdUnaryOp};
7
8use crate::exp::ReducedRangeExp;
9
10/// Computes the [softmax][softmax] function over a slice of floats.
11///
12/// The implementation uses a three-pass approach for numerical stability.
13/// See <https://ogunlao.github.io/2020/04/26/you_dont_really_know_softmax.html>.
14/// and <https://arxiv.org/abs/2001.04438>.
15///
16/// [softmax]: <https://en.wikipedia.org/wiki/Softmax_function>
17pub struct Softmax<'src, 'dst> {
18    src_dest: SrcDest<'src, 'dst, f32>,
19    flush_nans_to_zero: bool,
20}
21
22impl<'src, 'dst> Softmax<'src, 'dst> {
23    /// Construct a softmax operation which reads `input` and writes to to
24    /// `output`.
25    #[track_caller]
26    pub fn new(input: &'src [f32], output: &'dst mut [MaybeUninit<f32>]) -> Self {
27        Softmax {
28            src_dest: (input, output).into(),
29            flush_nans_to_zero: false,
30        }
31    }
32
33    /// Construct a softmax operation which updates `input` in place.
34    pub fn new_mut(input: &'dst mut [f32]) -> Self
35    where
36        'dst: 'src,
37    {
38        Softmax {
39            src_dest: input.into(),
40            flush_nans_to_zero: false,
41        }
42    }
43
44    /// Replace NaN values in the output with zeros.
45    ///
46    /// This option exists to changing handling of the case where the input
47    /// values are all negative infinity. In that case the normal output would
48    /// be NaN.
49    ///
50    /// In the context of attention operations which use negative infinity to
51    /// represent masked token positions, it is preferable to produce zeros as
52    /// the output if _all_ input positions are masked. See
53    /// <https://github.com/pytorch/pytorch/issues/41508>.
54    pub fn flush_nans_to_zero(mut self, flush: bool) -> Self {
55        self.flush_nans_to_zero = flush;
56        self
57    }
58}
59
60impl<'dst> SimdOp for Softmax<'_, 'dst> {
61    /// The normalized elements.
62    type Output = &'dst mut [f32];
63
64    #[inline(always)]
65    fn eval<I: Isa>(self, isa: I) -> Self::Output {
66        let ops = isa.f32();
67
68        let max_val = max(ops, self.src_dest.src());
69
70        // Compute `y = exp(x - max(x))` and `sum(y)`.
71        let (dest, exp_sum) = exp_sum_minus_max(isa, self.src_dest, max_val);
72
73        // Divide by `exp_sum`.
74        let exp_sum = ops.splat(exp_sum);
75        let inv_exp_sum = ops.reciprocal(exp_sum);
76        const UNROLL: usize = 2;
77        let zero = ops.zero();
78
79        if self.flush_nans_to_zero {
80            simd_apply::<_, _, _, UNROLL>(
81                ops,
82                dest,
83                #[inline(always)]
84                |x| {
85                    let y = ops.mul(x, inv_exp_sum);
86                    let not_nan = ops.eq(y, y);
87                    ops.select(y, zero, not_nan)
88                },
89            );
90        } else {
91            simd_apply::<_, _, _, UNROLL>(
92                ops,
93                dest,
94                #[inline(always)]
95                |x| ops.mul(x, inv_exp_sum),
96            );
97        }
98
99        dest
100    }
101}
102
103/// Computes the log softmax function over a slice of floats.
104///
105/// This is conceptually `log(softmax(x))` which can be rewritten as
106/// `log(exp(x) / sum(exp(x)))`.
107pub struct LogSoftmax<'src, 'dst> {
108    src_dest: SrcDest<'src, 'dst, f32>,
109}
110
111impl<'src, 'dst> LogSoftmax<'src, 'dst> {
112    /// Construct a log softmax operation which reads `input` and writes to
113    /// `output`.
114    pub fn new(input: &'src [f32], output: &'dst mut [MaybeUninit<f32>]) -> Self {
115        LogSoftmax {
116            src_dest: (input, output).into(),
117        }
118    }
119
120    /// Construct a log softmax operation which updates `input` in place.
121    pub fn new_mut(input: &'dst mut [f32]) -> Self
122    where
123        'dst: 'src,
124    {
125        LogSoftmax {
126            src_dest: input.into(),
127        }
128    }
129}
130
131impl<'dst> SimdOp for LogSoftmax<'_, 'dst> {
132    /// The normalized elements.
133    type Output = &'dst mut [f32];
134
135    #[inline(always)]
136    fn eval<I: Isa>(self, isa: I) -> Self::Output {
137        let ops = isa.f32();
138
139        // The maximum is subtracted from the input for numerical stability.
140        // Log identities are used to simplify the result:
141        //
142        // log(exp(xi - xmax) / sum(exp(x - xmax)))
143        //  = log(exp(xi - xmax)) - log(sum(exp(x - xmax)))
144        //  = xi - xmax - log(sum(exp(x - xmax)))
145        let max_val = max(ops, self.src_dest.src());
146
147        // Compute `sum(exp(x - max(x)))`.
148        let max_vec = ops.splat(max_val);
149        let exp_sum = self.src_dest.src().simd_iter(ops).fold(
150            ops.zero(),
151            #[inline(always)]
152            |exp_sum, x| {
153                // Use faster `exp(x)` since input is known to be <= 0.
154                let y = ReducedRangeExp::apply(isa, ops.sub(x, max_vec));
155                ops.add(exp_sum, y)
156            },
157        );
158        let exp_sum: f32 = exp_sum.to_array().into_iter().sum();
159
160        // Compute `y = (x - x_max) - log(exp_sum)`.
161        //
162        // We use two separate subtractions inside the loop instead of computing
163        // `x_max + log(exp_sum)` once and then using a single subtraction. This
164        // reduces rounding error when `|x_max|` is large and `log(exp_sum)` is
165        // small.
166        let log_exp_sum = ops.splat(exp_sum.ln());
167        simd_map(
168            ops,
169            self.src_dest,
170            #[inline(always)]
171            |x| ops.sub(ops.sub(x, max_vec), log_exp_sum),
172        )
173    }
174}
175
176/// Returns the maximum value in `xs`, or [`f32::MIN`] if `xs` is empty.
177#[inline(always)]
178fn max<O: FloatOps<f32>>(ops: O, xs: &[f32]) -> f32 {
179    let max_val = xs.simd_iter(ops).fold_unroll::<4>(
180        ops.splat(f32::MIN),
181        #[inline(always)]
182        |max, x| ops.max(max, x),
183        #[inline(always)]
184        |max, x| ops.max(max, x),
185    );
186    max_val
187        .to_array()
188        .into_iter()
189        .fold(f32::MIN, |max, x| max.max(x))
190}
191
192/// Computes `y = exp(x - max(x))` and `sum(y)` in a single pass.
193#[inline(always)]
194fn exp_sum_minus_max<'dst, I: Isa>(
195    isa: I,
196    src_dest: SrcDest<'_, 'dst, f32>,
197    max_val: f32,
198) -> (&'dst mut [f32], f32) {
199    let ops = isa.f32();
200
201    let max_val = ops.splat(max_val);
202
203    // *x = (*x - max_val).exp()
204    let mut prev_exp_sum = ops.zero();
205    let mut exp_sum = ops.zero();
206    let dest = simd_map(
207        ops,
208        src_dest,
209        #[inline(always)]
210        |x| {
211            // Use faster `exp(x)` since input is known to be <= 0.
212            let y = ReducedRangeExp::apply(isa, ops.sub(x, max_val));
213            prev_exp_sum = exp_sum;
214            exp_sum = ops.add(exp_sum, y);
215            y
216        },
217    );
218
219    // Undo the last update to `exp_sum` for unused lanes.
220    let remainder = dest.len() % ops.len();
221    if remainder != 0 {
222        let remainder_mask = ops.first_n_mask(remainder);
223        exp_sum = ops.select(exp_sum, prev_exp_sum, remainder_mask);
224    }
225    let exp_sum = exp_sum.to_array().into_iter().sum();
226
227    (dest, exp_sum)
228}
229
230#[cfg(test)]
231mod tests {
232    use rten_simd::SimdOp;
233
234    use super::{LogSoftmax, Softmax};
235    use crate::testing::{AsUninit, benchmark_op, check_f32s_are_equal_ulps, triples};
236
237    fn reference_log_softmax(xs: &[f32], ys: &mut [f32]) {
238        let max = xs.iter().copied().fold(f32::MIN, |max, x| max.max(x));
239        let log_exp_sum = xs
240            .iter()
241            .fold(0., |exp_sum: f32, x| exp_sum + (x - max).exp())
242            .ln();
243        for (x, y) in xs.iter().zip(ys.iter_mut()) {
244            *y = (*x - max) - log_exp_sum;
245        }
246    }
247
248    fn reference_softmax(xs: &[f32], ys: &mut [f32]) {
249        let max = xs.iter().copied().fold(f32::MIN, |max, x| max.max(x));
250
251        let mut exp_sum = 0.;
252        for (x, y) in xs.iter().zip(ys.iter_mut()) {
253            *y = (*x - max).exp();
254            exp_sum += *y;
255        }
256
257        for el in ys.iter_mut() {
258            *el /= exp_sum;
259        }
260    }
261
262    #[test]
263    fn test_softmax() {
264        // Test against reference values.
265        let input = vec![0.1634, 0.8647, 0.6401, 0.8265, 0.0560, 0.2304];
266        let expected = &([
267            0.11715934, 0.23623686, 0.18871443, 0.2273828, 0.10522857, 0.12527795,
268        ]);
269        let mut actual = vec![0.; input.len()];
270
271        Softmax::new(&input, actual.as_mut_slice().as_uninit()).dispatch();
272        check_f32s_are_equal_ulps(triples(&input, &actual, expected), 1. /* max ULPs */);
273
274        // Test against reference implementation for various lengths.
275        for len in 1..20 {
276            let input: Vec<f32> = (0..len).map(|x| x as f32 + 0.1).collect();
277            let mut expected = vec![0.; input.len()];
278            reference_softmax(&input, &mut expected);
279
280            let mut actual = vec![0.; input.len()];
281            Softmax::new(&input, actual.as_mut_slice().as_uninit()).dispatch();
282
283            check_f32s_are_equal_ulps(triples(&input, &actual, &expected), 3. /* max ULPs */);
284        }
285    }
286
287    #[test]
288    fn test_softmax_flush_nans_to_zero() {
289        let mut input = [f32::NEG_INFINITY; 3];
290        Softmax::new_mut(&mut input).dispatch();
291        assert!(input.iter().all(|x| x.is_nan()));
292
293        let mut input = [f32::NEG_INFINITY; 3];
294        Softmax::new_mut(&mut input)
295            .flush_nans_to_zero(true)
296            .dispatch();
297        assert_eq!(input, [0.; 3]);
298    }
299
300    #[test]
301    fn test_log_softmax() {
302        // Test against reference implementation for various lengths.
303        for len in 1..20 {
304            let input: Vec<f32> = (0..len).map(|x| x as f32 + 0.1).collect();
305            let mut expected = vec![0.; input.len()];
306            reference_log_softmax(&input, &mut expected);
307
308            let mut actual = vec![0.; input.len()];
309            LogSoftmax::new(&input, actual.as_mut_slice().as_uninit()).dispatch();
310
311            check_f32s_are_equal_ulps(triples(&input, &actual, &expected), 3. /* max ULPs */);
312        }
313    }
314
315    #[test]
316    fn test_log_softmax_in_place() {
317        let input: Vec<f32> = (0..32).map(|x| x as f32 * 0.5 - 8.).collect();
318        let mut expected = vec![0.; input.len()];
319        reference_log_softmax(&input, &mut expected);
320
321        let mut actual = input.clone();
322        LogSoftmax::new_mut(&mut actual).dispatch();
323
324        check_f32s_are_equal_ulps(triples(&input, &actual, &expected), 3. /* max ULPs */);
325    }
326
327    #[test]
328    fn test_log_softmax_sums_to_one() {
329        // `exp(log_softmax(x))` is a probability distribution, so it must sum
330        // to one regardless of the input scale.
331        for scale in [1e-3, 1., 10., 100.] {
332            let input: Vec<f32> = (0..64).map(|x| (x as f32 - 32.) * scale).collect();
333            let mut actual = input.clone();
334            LogSoftmax::new_mut(&mut actual).dispatch();
335
336            let sum: f32 = actual.iter().map(|x| x.exp()).sum();
337            assert!((sum - 1.).abs() < 1e-4, "scale {scale} sum {sum}");
338        }
339    }
340
341    #[test]
342    #[ignore]
343    fn bench_softmax() {
344        benchmark_op(reference_softmax, |src, dest| {
345            Softmax::new(src, dest).dispatch();
346        });
347    }
348
349    #[test]
350    #[ignore]
351    fn bench_log_softmax() {
352        benchmark_op(reference_log_softmax, |src, dest| {
353            LogSoftmax::new(src, dest).dispatch();
354        });
355    }
356}