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 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 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 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 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(); let out_qp = output_dt.qparams().unwrap(); 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 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
305struct 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 let mut buffer = vec![0_i32; view.len()];
381
382 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 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 *exp = exp_on_negative_values(scaled_input_diff);
401 });
402
403 let sum_of_exp = buffer.iter().map(|it| rescale(*it, 0, 12)).sum();
406
407 let (inv_sum_of_exp, num_bits_over_unit) = get_reciprocal(sum_of_exp, 12);
410
411 let exponent = num_bits_over_unit as isize + 31 - 8;
413
414 view.iter_mut().zip(buffer.iter()).for_each(|(it, exp)| {
415 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 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 #[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 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 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 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 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 fn test_softmax_trivial_0() -> Result<()> {
650 let input_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.03125 }); let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.00390625 }); 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 fn test_softmax_trivial_1() -> Result<()> {
663 let input_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0625 }); let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.00390625 }); 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 fn test_softmax_trivial_2() -> Result<()> {
676 let input_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0625 }); let output_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0078125 }); 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 fn test_softmax_trivial_3() -> Result<()> {
689 let input_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.03125 }); let output_dt = DatumType::QI8(ZpScale { zero_point: 0, scale: 0.0078125 }); 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 }); let output_dt = DatumType::QU8(ZpScale { zero_point: 0, scale: 0.5 }); 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 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 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 #[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}