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