Skip to main content

tract_core/ops/nn/softmax/
mod.rs

1mod fixedpoint;
2pub mod math;
3
4use math::{
5    convert_scale_to_mult_shift, exp_on_negative_values, get_reciprocal, rescale,
6    rounding_divide_by_pot, saturating_rounding_doubling_high_mul,
7    saturating_rounding_multiply_by_pot,
8};
9use num_traits::Float;
10use std::fmt::Debug;
11use tract_num_traits::Zero;
12
13use crate::internal::*;
14use ndarray::prelude::*;
15use tract_linalg::routines::Func;
16
17#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Default)]
18pub enum SoftmaxKind {
19    #[default]
20    Softmax,
21    LogSoftmax,
22}
23
24#[derive(Debug, Clone, new, Hash, Default, PartialEq, Eq)]
25pub struct Softmax {
26    pub axes: TVec<usize>,
27    pub quant_output_dt: Option<DatumType>,
28    pub kind: SoftmaxKind,
29}
30
31impl Op for Softmax {
32    fn name(&self) -> StaticName {
33        match self.kind {
34            SoftmaxKind::Softmax => "Softmax".into(),
35            SoftmaxKind::LogSoftmax => "LogSoftmax".into(),
36        }
37    }
38
39    fn info(&self) -> TractResult<Vec<String>> {
40        Ok(vec![format!("Axis: {:?}", self.axes)])
41    }
42
43    op_as_typed_op!();
44}
45
46impl TypedOp for Softmax {
47    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
48        let dt = inputs[0].datum_type;
49        if dt.is_float() {
50            ensure!(
51                self.quant_output_dt.is_none(),
52                "Float softmax should not have quant_output_dt, have {:?}",
53                self.quant_output_dt
54            );
55        } else if dt.is_quantized() {
56            ensure!(
57                self.quant_output_dt.map(|q| q.is_quantized()).unwrap_or(false),
58                "Quantized softmax should have a quantized output type (got {:?})",
59                self.quant_output_dt
60            );
61        } else {
62            bail!(
63                "Unsupported datum type in softmax: input type {:?}, output type {:?}",
64                dt,
65                self.quant_output_dt
66            );
67        }
68
69        let fact = self.quant_output_dt.unwrap_or(dt).fact(inputs[0].shape.clone());
70        Ok(tvec!(fact))
71    }
72
73    fn input_roi(
74        &self,
75        model: &TypedModel,
76        node: &TypedNode,
77    ) -> TractResult<Option<TVec<Option<TDim>>>> {
78        crate::optim::propagate_roi::bubble_roi(model, node)
79    }
80
81    fn axes_mapping(
82        &self,
83        inputs: &[&TypedFact],
84        outputs: &[&TypedFact],
85    ) -> TractResult<AxesMapping> {
86        AxesMapping::natural(inputs, outputs)
87    }
88
89    fn change_axes(
90        &self,
91        model: &TypedModel,
92        node: &TypedNode,
93        _io: InOut,
94        change: &AxisOp,
95    ) -> TractResult<Option<AxisChangeConsequence>> {
96        let axes: Option<TVec<usize>> =
97            self.axes.iter().map(|it| change.transform_axis(*it)).collect();
98        if let Some(axes) = axes {
99            Ok(Some(AxisChangeConsequence::new(
100                model,
101                node,
102                Some(Box::new(Softmax { axes, ..self.clone() })),
103                change,
104            )))
105        } else {
106            Ok(None)
107        }
108    }
109
110    as_op!();
111}
112
113impl EvalOp for Softmax {
114    op_out_of_plan!();
115
116    fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
117        let input = args_1!(inputs);
118        let dt = input.datum_type();
119
120        let output = match dt {
121            DatumType::F64 => self.eval_t::<f64>(input)?,
122            DatumType::F32 => self.eval_t::<f32>(input)?,
123            DatumType::F16 => self.eval_t::<f16>(input)?,
124            DatumType::QI8(_) | DatumType::QU8(_) => self.eval_quant(input)?,
125            dt => bail!("Unsupported type {dt:?}"),
126        };
127        Ok(output)
128    }
129}
130
131impl Softmax {
132    fn eval_t<T>(&self, input: TValue) -> TractResult<TVec<TValue>>
133    where
134        T: Float + Datum + std::iter::Sum,
135    {
136        let mut output = input.into_tensor();
137
138        // Fast path: when the softmax axes are the trailing axes of a
139        // C-contiguous tensor, the buffer is [n_rows, row_len] row-major
140        // (row_len = product of the softmax dims), so every softmax region is
141        // one contiguous row. Rows split across threads while each row's
142        // reduction stays serial, keeping the result bit-identical.
143        let rank = output.rank();
144        let mut sm_axes: TVec<usize> = self.axes.clone();
145        sm_axes.sort_unstable();
146        let trailing = !sm_axes.is_empty()
147            && sm_axes.len() <= rank
148            && sm_axes.iter().enumerate().all(|(i, &a)| a == rank - sm_axes.len() + i);
149        if trailing
150            && matches!(T::datum_type(), DatumType::F32 | DatumType::F16)
151            && output.strides() == &*Tensor::natural_strides(output.shape())
152        {
153            let row_len: usize = sm_axes.iter().map(|&a| output.shape()[a]).product();
154            if row_len > 0 {
155                let kind = self.kind;
156                let mut output_plain = output.try_as_plain_ram_mut()?;
157                // Dtype-concrete `as_slice_mut` (checked, no transmute) since T is
158                // f32/f16 here; keeps this core path free of new `unsafe`.
159                if T::datum_type() == f32::datum_type()
160                    && kind == SoftmaxKind::Softmax
161                    && row_len.is_multiple_of(4)
162                    && let Some(rows_kernel) = tract_linalg::routines::softmax_rows_f32()
163                {
164                    let data = output_plain.as_slice_mut::<f32>()?;
165                    let total = data.len();
166                    tract_linalg::multithread::par_chunks_mut(data, row_len, total, |_, chunk| {
167                        rows_kernel(chunk, row_len);
168                        Ok(())
169                    })?;
170                } else if T::datum_type() == f32::datum_type() {
171                    let data = output_plain.as_slice_mut::<f32>()?;
172                    let total = data.len();
173                    let kernels = F32Kernels::new()?;
174                    tract_linalg::multithread::par_chunks_mut(data, row_len, total, |_, chunk| {
175                        for row in chunk.chunks_mut(row_len) {
176                            softmax_inner_slice_f32(row, kind, &kernels)?;
177                        }
178                        Ok(())
179                    })?;
180                } else {
181                    let data = output_plain.as_slice_mut::<f16>()?;
182                    let total = data.len();
183                    tract_linalg::multithread::par_chunks_mut(data, row_len, total, |_, chunk| {
184                        for row in chunk.chunks_mut(row_len) {
185                            self.softmax_inner_slice_f16(row, kind)?;
186                        }
187                        Ok(())
188                    })?;
189                }
190                return Ok(tvec!(output.into_tvalue()));
191            }
192        }
193
194        // Slow path: strided or non-trailing axes, iterated with ndarray view
195        // collapsing (the contiguous-row fast path above did not apply).
196        let mut iterating_shape: TVec<usize> = output.shape().into();
197
198        for i in 0..iterating_shape.len() {
199            if self.axes.contains(&i) {
200                iterating_shape[i] = 1
201            }
202        }
203
204        let mut output_plain = output.try_as_plain_ram_mut()?;
205        let mut view = output_plain.to_array_view_mut::<T>()?;
206        let kernels =
207            if T::datum_type() == f32::datum_type() { Some(F32Kernels::new()?) } else { None };
208
209        for it_coords in tract_ndarray::indices(&*iterating_shape) {
210            let mut view = view.view_mut();
211            for ix in 0..iterating_shape.len() {
212                if !self.axes.contains(&ix) {
213                    view.collapse_axis(Axis(ix), it_coords[ix]);
214                }
215            }
216            if let Some(slice) =
217                view.as_slice_mut().filter(|_| T::datum_type() == f32::datum_type())
218            {
219                let slice: &mut [f32] = unsafe { std::mem::transmute(slice) };
220                softmax_inner_slice_f32(
221                    slice,
222                    self.kind,
223                    kernels.as_ref().context("f32 kernels")?,
224                )?;
225            } else if let Some(slice) =
226                view.as_slice_mut().filter(|_| T::datum_type() == f16::datum_type())
227            {
228                let slice: &mut [f16] = unsafe { std::mem::transmute(slice) };
229                self.softmax_inner_slice_f16(slice, self.kind)?;
230            } else {
231                softmax_inner(view, self.kind);
232            }
233        }
234
235        Ok(tvec!(output.into_tvalue()))
236    }
237
238    fn eval_quant(&self, input: TValue) -> TractResult<TVec<TValue>> {
239        if self.kind == SoftmaxKind::LogSoftmax {
240            bail!("Quantized LogSoftmax is not supported")
241        }
242        let mut iterating_shape: TVec<usize> = input.shape().into();
243        let output_dt =
244            self.quant_output_dt.context("Quandized softmax eval with no output type")?;
245
246        for i in 0..iterating_shape.len() {
247            if self.axes.contains(&i) {
248                iterating_shape[i] = 1
249            }
250        }
251
252        // All operations will be done in u8, we will cast the result appropriately afterward.
253        let src_is_signed = input.datum_type().is_signed();
254        let out_is_signed = output_dt.is_signed();
255        let in_qp = input.datum_type().qparams().unwrap(); // Checked as we are in the quant case
256        let out_qp = output_dt.qparams().unwrap(); // Checked as we are in the quant case
257        let mut output = unsafe { input.into_tensor().into_array_unchecked::<u8>() };
258
259        for it_coords in tract_ndarray::indices(&*iterating_shape) {
260            let mut view = output.view_mut();
261            for ix in 0..iterating_shape.len() {
262                if !self.axes.contains(&ix) {
263                    view.collapse_axis(Axis(ix), it_coords[ix]);
264                }
265            }
266            softmax_quant_inner(view, src_is_signed, in_qp, out_is_signed, out_qp);
267        }
268
269        let mut output_tensor = output.into_tensor();
270        unsafe { output_tensor.set_datum_type(output_dt) };
271        Ok(tvec!(output_tensor.into_tvalue()))
272    }
273
274    fn softmax_inner_slice_f16(&self, slice: &mut [f16], kind: SoftmaxKind) -> TractResult<()> {
275        let max = Func::ReduceMax.reduce_f16()?.run(slice)?;
276        match kind {
277            SoftmaxKind::Softmax => {
278                // f16 softmax runs the scalar libm exp and accumulates the sum
279                // in f32: a row long enough for the running sum to outgrow its
280                // own terms stalls in f16, costing double digits of relative
281                // error by a few thousand elements. A vectorized f16 exp is not
282                // worth its fragility, f16 softmax always lands here.
283                let mut sum = 0f32;
284                slice.iter_mut().for_each(|x| {
285                    *x = (*x - max).exp();
286                    sum += x.to_f32();
287                });
288                let rsum = f16::from_f32(sum.recip());
289                Func::MulByScalar.ew_f16_param()?.run_with_params(slice, rsum)?;
290            }
291            SoftmaxKind::LogSoftmax => {
292                let mut exp_sum = 0f32;
293                slice.iter_mut().for_each(|x| {
294                    *x -= max;
295                    exp_sum += x.exp().to_f32();
296                });
297                let log_sum = f16::from_f32(exp_sum.ln());
298                slice.iter_mut().for_each(|x| *x -= log_sum);
299            }
300        }
301        Ok(())
302    }
303}
304
305/// The f32 row kernels, built once per evaluation: a softmax over many short rows would
306/// otherwise build them again for every row.
307struct F32Kernels {
308    max: Box<dyn tract_linalg::reduce::Reduce<f32>>,
309    exp_sum: Box<dyn tract_linalg::reduce::MapReduce<f32, f32>>,
310    scale: Box<dyn tract_linalg::element_wise::ElementWise<f32, f32>>,
311}
312
313impl F32Kernels {
314    fn new() -> TractResult<F32Kernels> {
315        Ok(F32Kernels {
316            max: Func::ReduceMax.reduce_f32()?,
317            exp_sum: Func::Softmax2.map_reduce_f32()?,
318            scale: Func::MulByScalar.ew_f32_param()?,
319        })
320    }
321}
322
323fn softmax_inner_slice_f32(
324    slice: &mut [f32],
325    kind: SoftmaxKind,
326    kernels: &F32Kernels,
327) -> TractResult<()> {
328    let max = kernels.max.run(slice)?;
329    match kind {
330        SoftmaxKind::Softmax => {
331            let sum = kernels.exp_sum.run_with_params(slice, max)?;
332            kernels.scale.run_with_params(slice, sum.recip())?;
333        }
334        SoftmaxKind::LogSoftmax => {
335            let mut exp_sum = f32::zero();
336            slice.iter_mut().for_each(|x| {
337                *x -= max;
338                exp_sum += x.exp();
339            });
340            let log_sum = exp_sum.ln();
341            slice.iter_mut().for_each(|x| *x -= log_sum);
342        }
343    }
344    Ok(())
345}
346
347fn softmax_inner<T: Float + Datum + std::iter::Sum, D: Dimension>(
348    mut view: ArrayViewMut<T, D>,
349    kind: SoftmaxKind,
350) {
351    let max =
352        *view.iter().max_by(|i, j| i.partial_cmp(j).unwrap_or(std::cmp::Ordering::Less)).unwrap();
353    view.mapv_inplace(|x| x - max);
354    let exp_sum = view.iter().map(|&x| x.exp()).sum();
355    match kind {
356        SoftmaxKind::Softmax => {
357            view.mapv_inplace(|x| x.exp() / exp_sum);
358        }
359        SoftmaxKind::LogSoftmax => {
360            let log_sum = exp_sum.ln();
361            view.mapv_inplace(|x| x - log_sum);
362        }
363    }
364}
365
366fn softmax_quant_inner<D: Dimension>(
367    mut view: ArrayViewMut<u8, D>,
368    src_is_signed: bool,
369    in_qp: QParams,
370    out_is_signed: bool,
371    out_qp: QParams,
372) {
373    let (_, in_scale) = in_qp.zp_scale();
374    let (scale_in_multiplier, scale_in_shift) = convert_scale_to_mult_shift(in_scale).unwrap();
375    let (_, out_scale) = out_qp.zp_scale();
376    let (scale_out_multiplier, scale_out_shift) = convert_scale_to_mult_shift(out_scale).unwrap();
377    let shift = 26 - scale_in_shift;
378
379    // Compute the exponentials x - max
380    let mut buffer = vec![0_i32; view.len()];
381
382    // Handle the case were we considered an i8 as an u8 and still get the right x - max.
383    let safe_u8 = if src_is_signed { |x: &u8| x.wrapping_add(128) } else { |x: &u8| *x };
384
385    let max = view.iter().map(safe_u8).max().unwrap();
386    view.iter().zip(buffer.iter_mut()).for_each(|(x, exp)| {
387        let input_diff = safe_u8(x) as i32 - max as i32;
388
389        // We scale the input to be in Q5_26
390        let scaled_input_diff = if scale_in_multiplier != 0 {
391            saturating_rounding_multiply_by_pot(
392                saturating_rounding_doubling_high_mul(input_diff, scale_in_multiplier),
393                shift as i32,
394            )
395        } else {
396            saturating_rounding_multiply_by_pot(input_diff, shift as i32)
397        };
398
399        // It expects an input from Q5_26 and returns an output in Q0_31
400        *exp = exp_on_negative_values(scaled_input_diff);
401    });
402
403    // Compute sum of exp
404    // The sum is stored as an Q12_19 that's why we need to recale from Q0_31 to Q12_19 before summing.
405    let sum_of_exp = buffer.iter().map(|it| rescale(*it, 0, 12)).sum();
406
407    // Compute 1/sum_of_exp
408    // The result of this function is in Q0_31
409    let (inv_sum_of_exp, num_bits_over_unit) = get_reciprocal(sum_of_exp, 12);
410
411    // Compute the exponent value needed to be in Q24_8 before the final rescaling
412    let exponent = num_bits_over_unit as isize + 31 - 8;
413
414    view.iter_mut().zip(buffer.iter()).for_each(|(it, exp)| {
415        // Compute the product of exp * 1/sum_of_exp and scale the result in Q24_8
416        let unsat_output = rounding_divide_by_pot(
417            saturating_rounding_doubling_high_mul(inv_sum_of_exp, *exp),
418            exponent as i32,
419        );
420
421        // Scale the final result in the output scale range
422        let unsat_scaled_output = {
423            if scale_out_multiplier != 0 {
424                let (inv_multiplier, num_bits) = get_reciprocal(scale_out_multiplier, 1);
425                rounding_divide_by_pot(
426                    saturating_rounding_doubling_high_mul(unsat_output, inv_multiplier),
427                    (8 - scale_out_shift - 1 - num_bits as isize) as i32,
428                )
429            } else {
430                rounding_divide_by_pot(unsat_output, (8 - scale_out_shift) as i32)
431            }
432        };
433
434        // Return the final result by clipping the computed value within its range
435        // and casting it to u8 in any case.
436        #[allow(unknown_lints, unnecessary_transmutes)]
437        if out_is_signed {
438            *it = unsafe {
439                std::mem::transmute::<i8, u8>(i32::max(
440                    i32::min(unsat_scaled_output, i8::MAX as i32),
441                    i8::MIN as i32,
442                ) as i8)
443            };
444        } else {
445            *it = i32::max(i32::min(unsat_scaled_output, u8::MAX as i32), u8::MIN as i32) as u8;
446        }
447    });
448}
449
450#[cfg(test)]
451mod test {
452    use super::*;
453    use crate::ops::nn::DataFormat::NCHW;
454    use anyhow::Result;
455    use num_traits::PrimInt;
456    use proptest::collection::vec;
457    use proptest::prelude::*;
458    use tract_data::internal::QParams::ZpScale;
459
460    fn assert_is_close(found: f32, expected: f32, in_dt: DatumType, out_dt: DatumType) {
461        let (_, in_epsilon) = in_dt.zp_scale();
462        let (_, out_epsilon) = out_dt.zp_scale();
463        let epsilon = in_epsilon + out_epsilon;
464        let error = (found - expected).abs();
465        assert!(
466            error <= epsilon,
467            "epsilon eq failed: |{found:?}-{expected:?}|={error} should be <= {epsilon}"
468        );
469    }
470
471    // Generate a random tensor with a quantized datum type
472    fn qtensor<T: PrimInt + Datum + Arbitrary>(shape: Vec<usize>) -> BoxedStrategy<Tensor> {
473        let len = shape.iter().product::<usize>();
474        let dt = q_datum::<T>((0.0001f32..0.1).boxed());
475        (vec(any::<T>(), len..=len), dt)
476            .prop_map(move |(vec, dt)| (ArrayD::from_shape_vec(shape.clone(), vec).unwrap(), dt))
477            .prop_map(move |(array, dt)| {
478                let mut tensor = array.into_tensor();
479                unsafe { tensor.set_datum_type(dt) };
480                tensor
481            })
482            .boxed()
483    }
484
485    // Generate a random quantized datum type
486    fn q_datum<T: PrimInt + Datum>(range: BoxedStrategy<f32>) -> BoxedStrategy<DatumType> {
487        let max_integer_bits = std::mem::size_of::<T>() * 8 - T::datum_type().is_signed() as usize;
488        prop_oneof![
489            (1usize..max_integer_bits).prop_map(|fixed_point| { 2f32.powi(-(fixed_point as i32)) }),
490            range
491        ]
492        .prop_map(|scale| {
493            if T::datum_type().is_signed() {
494                DatumType::QI8(ZpScale { zero_point: 0, scale })
495            } else {
496                DatumType::QU8(ZpScale { zero_point: 0, scale })
497            }
498        })
499        .boxed()
500    }
501
502    #[derive(Debug)]
503    struct SoftmaxProblem {
504        data: Tensor,
505        axes: TVec<usize>,
506        output_dt: DatumType,
507    }
508
509    impl SoftmaxProblem {
510        fn check(&self) -> Result<()> {
511            let inputs = tvec!(self.data.clone().into_tvalue());
512            let quant_output_dt = Some(self.output_dt).filter(|dt| !dt.is_float());
513            let softmax =
514                Softmax { axes: self.axes.clone(), quant_output_dt, ..Softmax::default() };
515
516            // Compute quantized output
517            let result = softmax.eval(&EvalContext::out_of_plan(), inputs)?;
518            let result = args_1!(result);
519            let result_float = result.cast_to::<f32>()?;
520
521            // Compute reference output
522            let input_float = self.data.cast_to::<f32>()?;
523            let inputs_float = tvec!(input_float.into_owned().into_tvalue());
524            let softmax_float = Softmax { axes: self.axes.clone(), ..Softmax::default() };
525            let reference_float = softmax_float.eval(&EvalContext::out_of_plan(), inputs_float)?;
526            let reference_array = args_1!(reference_float);
527            let reference = reference_array.to_plain_array_view::<f32>()?;
528
529            result_float
530                .to_plain_array_view::<f32>()?
531                .iter()
532                .zip(reference.iter())
533                .for_each(|(a, b)| assert_is_close(*a, *b, self.data.datum_type(), self.output_dt));
534            Ok(())
535        }
536    }
537
538    impl Arbitrary for SoftmaxProblem {
539        type Parameters = ();
540        type Strategy = BoxedStrategy<SoftmaxProblem>;
541        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
542            (1usize..2, 1usize..2, 1usize..5, 1usize..5, 0usize..4)
543                .prop_flat_map(|(n, c, h, w, axis)| {
544                    let shape_in: Vec<usize> =
545                        NCHW.from_n_c_hw(n, c, [h, w]).unwrap().shape.to_vec();
546                    (
547                        prop_oneof![qtensor::<i8>(shape_in.clone()), qtensor::<u8>(shape_in)],
548                        Just(tvec![axis]),
549                        prop_oneof![
550                            q_datum::<u8>((0.008f32..0.1).boxed()),
551                            q_datum::<i8>((0.008f32..0.1).boxed())
552                        ],
553                    )
554                })
555                .prop_map(|(data, axes, output_dt)| SoftmaxProblem { data, axes, output_dt })
556                .boxed()
557        }
558    }
559
560    #[derive(Debug)]
561    pub struct InnerSoftmaxProblem {
562        in_qp: QParams,
563        out_qp: QParams,
564        data: Vec<i8>,
565    }
566
567    impl InnerSoftmaxProblem {
568        fn check(&self) -> Result<()> {
569            let quantized = self.quantized();
570            let reference = self.reference();
571            assert!(quantized.iter().zip(reference.iter()).all(|(quantized, expected)| {
572                let abs_diff = if *quantized > *expected {
573                    quantized - *expected
574                } else {
575                    expected - *quantized
576                };
577                abs_diff <= 1
578            }));
579            Ok(())
580        }
581
582        fn reference(&self) -> Vec<u8> {
583            let (in_zero_point, in_scale) = self.in_qp.zp_scale();
584            let (out_zero_point, out_scale) = self.out_qp.zp_scale();
585            let in_float =
586                self.data.iter().map(|it| (*it as f32 - in_zero_point as f32) * in_scale).collect();
587            let mut in_float_array = Array1::from_vec(in_float);
588            softmax_inner(in_float_array.view_mut(), SoftmaxKind::default());
589            in_float_array
590                .iter()
591                .map(|it| {
592                    ((*it / out_scale).round() as i32 + out_zero_point)
593                        .max(u8::MIN as i32)
594                        .min(u8::MAX as i32) as u8
595                })
596                .collect()
597        }
598
599        fn quantized(&self) -> Vec<u8> {
600            let in_data: Vec<u8> = unsafe { std::mem::transmute(self.data.clone()) };
601            let mut in_array = Array1::from_vec(in_data);
602            softmax_quant_inner(in_array.view_mut(), true, self.in_qp, false, self.out_qp);
603            in_array.to_vec()
604        }
605    }
606
607    impl Arbitrary for InnerSoftmaxProblem {
608        type Parameters = ();
609        type Strategy = BoxedStrategy<InnerSoftmaxProblem>;
610        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
611            (
612                prop_oneof![
613                    q_datum::<i8>((0.0001f32..0.01).boxed()),
614                    q_datum::<u8>((0.0001f32..0.01).boxed())
615                ],
616                prop_oneof![
617                    q_datum::<u8>((0.008f32..0.1).boxed()),
618                    q_datum::<i8>((0.008f32..0.1).boxed())
619                ],
620                vec(any::<i8>(), 1..10),
621            )
622                .prop_map(|(in_qp, out_qp, data)| InnerSoftmaxProblem {
623                    in_qp: in_qp.qparams().unwrap(),
624                    out_qp: out_qp.qparams().unwrap(),
625                    data,
626                })
627                .boxed()
628        }
629    }
630
631    proptest::proptest! {
632        #![proptest_config(ProptestConfig::with_cases(1000))]
633        #[test]
634        fn test_softmax_inner_prop(pb in any::<InnerSoftmaxProblem>()) {
635            pb.check().unwrap()
636        }
637    }
638
639    proptest::proptest! {
640        #![proptest_config(ProptestConfig::with_cases(1000))]
641        #[test]
642        fn test_softmax_prop(pb in any::<SoftmaxProblem>()) {
643            pb.check().unwrap()
644        }
645    }
646
647    #[test]
648    // We test QU8 -> QU8
649    fn test_softmax_trivial_0() -> Result<()> {
650        let input_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.03125 }); // Q3_5
651        let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.00390625 }); // Q0_8;
652        let mut data = Tensor::from_shape(&[1, 1, 2, 2], &[0_u8, 0, 0, 4])?;
653        unsafe { data.set_datum_type(input_dt) };
654
655        let prob = SoftmaxProblem { data, axes: tvec![3], output_dt };
656        prob.check()?;
657        Ok(())
658    }
659
660    #[test]
661    // We test QI8 -> QU8
662    fn test_softmax_trivial_1() -> Result<()> {
663        let input_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0625 }); // Q3_4
664        let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.00390625 }); // Q0_8;
665        let mut data = Tensor::from_shape(&[1, 1, 2, 2], &[0_i8, 0, 0, 4])?;
666        unsafe { data.set_datum_type(input_dt) };
667
668        let prob = SoftmaxProblem { data, axes: tvec![3], output_dt };
669        prob.check()?;
670        Ok(())
671    }
672
673    #[test]
674    // We test QI8 -> QI8
675    fn test_softmax_trivial_2() -> Result<()> {
676        let input_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0625 }); // Q3_4
677        let output_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0078125 }); // Q0_7;
678        let mut data = Tensor::from_shape(&[1, 1, 2, 2], &[0_i8, 0, 0, -4])?;
679        unsafe { data.set_datum_type(input_dt) };
680
681        let prob = SoftmaxProblem { data, axes: tvec![3], output_dt };
682        prob.check()?;
683        Ok(())
684    }
685
686    #[test]
687    // We test QU8 -> QI8
688    fn test_softmax_trivial_3() -> Result<()> {
689        let input_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.03125 }); // Q3_5
690        let output_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0078125 }); // Q0_7;
691        let mut data = Tensor::from_shape(&[1, 1, 2, 2], &[0_u8, 0, 0, 4])?;
692        unsafe { data.set_datum_type(input_dt) };
693
694        let prob = SoftmaxProblem { data, axes: tvec![2], output_dt };
695        prob.check()?;
696        Ok(())
697    }
698
699    #[test]
700    fn test_softmax_1() -> Result<()> {
701        let input_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.5 }); // Q6_1
702        let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.5 }); // Q7_1
703        let mut data = Tensor::from_shape(&[1, 1, 1, 2], &[115_i8, 115])?;
704        unsafe { data.set_datum_type(input_dt) };
705
706        let prob = SoftmaxProblem { data, axes: tvec![3], output_dt };
707        prob.check()?;
708        Ok(())
709    }
710
711    #[test]
712    fn test_softmax_2() -> Result<()> {
713        let input_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0001 });
714        let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.008 });
715        let mut data = Tensor::from_shape(&[1, 1, 1, 2], &[115_i8, 115])?;
716        unsafe { data.set_datum_type(input_dt) };
717
718        let prob = SoftmaxProblem { data, axes: tvec![3], output_dt };
719        prob.check()?;
720        Ok(())
721    }
722
723    #[test]
724    fn test_softmax_3() -> Result<()> {
725        let input_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.6220956 });
726        let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.5187921 });
727        let mut data = Tensor::from_shape(&[1, 1, 1, 2], &[13_u8, 218])?;
728        unsafe { data.set_datum_type(input_dt) };
729
730        let prob = SoftmaxProblem { data, axes: tvec![3], output_dt };
731        prob.check()?;
732        Ok(())
733    }
734
735    #[test]
736    fn test_inner_softmax_1() -> Result<()> {
737        let in_qp = ZpScale { zero_point: 0, scale: 0.03125 };
738        let out_qp = ZpScale { zero_point: 0, scale: 0.5 };
739        let data = vec![0_i8, 1];
740
741        let prob = InnerSoftmaxProblem { in_qp, out_qp, data };
742        prob.check()?;
743        Ok(())
744    }
745
746    #[test]
747    fn test_inner_softmax_2() -> Result<()> {
748        let in_qp = ZpScale { zero_point: 0, scale: 0.5 };
749        let out_qp = ZpScale { zero_point: 0, scale: 0.03125 };
750        let data = vec![100i8, -28];
751
752        let prob = InnerSoftmaxProblem { in_qp, out_qp, data };
753        prob.check()?;
754        Ok(())
755    }
756
757    #[test]
758    fn test_inner_softmax_not_pow_2_1() -> Result<()> {
759        let in_qp = ZpScale { zero_point: 0, scale: 0.7298456 };
760        let out_qp = ZpScale { zero_point: 0, scale: 0.03125 };
761        let data = vec![100i8, -28];
762
763        let prob = InnerSoftmaxProblem { in_qp, out_qp, data };
764        prob.check()?;
765        Ok(())
766    }
767
768    #[test]
769    #[ignore]
770    // Fails but the difference is quite low and the sum still give exactly one:
771    // quantized: 110(0.88), 15(0.12)
772    // expected: 112(0.896), 13(0.104)
773    fn test_inner_softmax_not_pow_2_2() -> Result<()> {
774        let in_qp = ZpScale { zero_point: 0, scale: 0.2123116 };
775        let out_qp = ZpScale { zero_point: 0, scale: 0.008 };
776        let data = vec![118i8, 108];
777
778        let prob = InnerSoftmaxProblem { in_qp, out_qp, data };
779        prob.check()?;
780        Ok(())
781    }
782
783    #[test]
784    #[ignore]
785    // Fails but the difference is quite low and the sum still give exactly one:
786    // quantized: 40(0.625), 24(0.375)
787    // expected: 42(0.65625), 22(0.34375)
788    fn test_inner_softmax_not_pow_2_3() -> Result<()> {
789        let in_qp = ZpScale { zero_point: 0, scale: 0.33034274 };
790        let out_qp = ZpScale { zero_point: 0, scale: 0.015625 };
791        let data = vec![45i8, 43];
792
793        let prob = InnerSoftmaxProblem { in_qp, out_qp, data };
794        prob.check()?;
795        Ok(())
796    }
797}
798
799#[cfg(test)]
800mod f16_accumulator {
801    use super::*;
802
803    /// The f16 row must stay close to the same softmax evaluated in f32. A row
804    /// long enough for the running sum to outgrow its own terms is the case an
805    /// f16 accumulator silently drops.
806    #[test]
807    fn long_rows_keep_their_normalisation() -> TractResult<()> {
808        for len in [1024usize, 4096, 8192] {
809            let logits: Vec<f32> = (0..len).map(|i| ((i as f32) * 0.7).sin() * 0.5).collect();
810
811            let mut half: Vec<f16> = logits.iter().map(|v| f16::from_f32(*v)).collect();
812            Softmax::new(tvec![0], None, SoftmaxKind::Softmax)
813                .softmax_inner_slice_f16(&mut half, SoftmaxKind::Softmax)?;
814
815            let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
816            let exps: Vec<f32> = logits.iter().map(|v| (v - max).exp()).collect();
817            let total: f32 = exps.iter().sum();
818
819            let got: f32 = half.iter().map(|v| v.to_f32()).sum();
820            assert!((got - 1.0).abs() < 0.02, "len {len}: row sums to {got}, expected 1.0");
821            for (i, (h, e)) in half.iter().zip(&exps).enumerate() {
822                let want = e / total;
823                let err = (h.to_f32() - want).abs() / want.max(f32::MIN_POSITIVE);
824                assert!(err < 0.05, "len {len} lane {i}: got {h}, want {want}, rel {err}");
825            }
826        }
827        Ok(())
828    }
829}