@@ -825,6 +825,116 @@ void SvdfQuantized1x16Input64x1OutputReluShouldMatchGolden() {
825825 sizeof (tflite::testing::golden_output_relu_16x1x1) / sizeof (float ));
826826}
827827
828+ // Regression test for a bug where the int8 activation-state path in
829+ // svdf_common.cc added the (potentially non-zero) activation_state
830+ // zero-point AFTER clamping the requantized dot product to the int8 range,
831+ // instead of before. This let the zero-point-shifted sum silently wrap
832+ // around the int8 range when narrowed into the persistent activation_state
833+ // buffer, instead of saturating like every other quantized op does.
834+ // See https://github.com/tensorflow/tflite-micro/issues/2721.
835+ //
836+ // Note: this deliberately builds its own tensors (rather than reusing
837+ // TestIntegerSVDF() above) because that helper always constructs the
838+ // activation_state tensor with a hardcoded zero-point of 0, regardless of
839+ // the activation_state_zero_point argument passed to it, and so never
840+ // actually exercises the non-zero zero-point code path this bug lives in.
841+ //
842+ // All quantization scales below are exactly 1.0 so that
843+ // MultiplyByQuantizedMultiplier() is an exact identity transform for the
844+ // in-range integer values used here. This keeps the expected values exact
845+ // and isolates the zero-point-add-vs-clamp ordering bug from any
846+ // requantization rounding behavior.
847+ void SvdfInt8ActivationStateZeroPointOverflowRegressionTest () {
848+ constexpr int batch_size = 1 ;
849+ constexpr int num_units = 1 ;
850+ constexpr int input_size = 1 ;
851+ constexpr int memory_size = 1 ;
852+ constexpr int rank = 1 ;
853+ constexpr int num_filters = num_units * rank;
854+
855+ const float input_scale = 1 .0f ;
856+ const int input_zero_point = 0 ;
857+ const float feature_weights_scale = 1 .0f ;
858+ const float time_weights_scale = 1 .0f ;
859+ const float activation_state_scale = 1 .0f ;
860+ const int activation_state_zero_point = 30 ; // Ordinary nonzero zero-point.
861+ const float output_scale = 1 .0f ;
862+ const int output_zero_point = 0 ;
863+
864+ // Raw feature-matmul dot product = feature_weight * input = 120 * 1 = 120,
865+ // unchanged by requantization (identity scale). Correct (saturating)
866+ // result stored into activation_state:
867+ // clamp(zero_point + dot_prod, -128, 127) = clamp(30 + 120, ...) = 127.
868+ // Buggy (add-after-clamp) result: clamp(dot_prod, ...) = 120 (no-op,
869+ // already in range), then int8_t(30 + 120) = int8_t(150) wraps to -106.
870+ const float input_data[input_size * batch_size] = {1 .0f };
871+ const float feature_weights_data[num_filters * input_size] = {120 .0f };
872+ const float time_weights_data[num_filters * memory_size] = {1 .0f };
873+ const float bias_data[num_units] = {0 .0f };
874+ const float initial_activation_state_data[batch_size * memory_size *
875+ num_filters] = {0 .0f };
876+ // Correct expected output, derived by hand from the corrected math above:
877+ // activation_state (after fix) = 127
878+ // time-step scratch = time_weight * (127 - zero_point) = 1 * 97 = 97
879+ // reduce (no bias) = 97; rescale (identity, scale=1.0) = 97;
880+ // + output_zero_point(0), clamped to int8 range = 97.
881+ const float golden_output[batch_size * num_units] = {97 .0f };
882+
883+ int8_t input_quantized[input_size * batch_size];
884+ int8_t feature_weights_quantized[num_filters * input_size];
885+ int8_t time_weights_quantized[num_filters * memory_size];
886+ int32_t bias_quantized[num_units];
887+ int8_t activation_state_quantized[batch_size * memory_size * num_filters];
888+ int8_t output_data[batch_size * num_units];
889+ int8_t golden_output_quantized[batch_size * num_units];
890+ int8_t input_sequences_quantized[input_size * batch_size];
891+
892+ int input_dims_arg[] = {2 , batch_size, input_size};
893+ TfLiteIntArray* input_dims = IntArrayFromInts (input_dims_arg);
894+ int feature_weights_dims_args[] = {2 , num_filters, input_size};
895+ TfLiteIntArray* feature_weights_dims =
896+ IntArrayFromInts (feature_weights_dims_args);
897+ int time_weights_dims_args[] = {2 , num_filters, memory_size};
898+ TfLiteIntArray* time_weights_dims = IntArrayFromInts (time_weights_dims_args);
899+ int bias_dims_data[] = {1 , num_units};
900+ TfLiteIntArray* bias_dims = IntArrayFromInts (bias_dims_data);
901+ int activation_state_dims_args[] = {2 , batch_size, memory_size * num_filters};
902+ TfLiteIntArray* activation_state_dims =
903+ IntArrayFromInts (activation_state_dims_args);
904+ int output_dims_args[] = {2 , batch_size, num_units};
905+ TfLiteIntArray* output_dims = IntArrayFromInts (output_dims_args);
906+
907+ const int tensor_count = 6 ; // 5 inputs, 1 output.
908+ TfLiteTensor tensors[] = {
909+ CreateQuantizedTensor (input_data, input_quantized, input_dims,
910+ input_scale, input_zero_point),
911+ CreateQuantizedTensor (feature_weights_data, feature_weights_quantized,
912+ feature_weights_dims, feature_weights_scale, 0 ),
913+ CreateQuantizedTensor (time_weights_data, time_weights_quantized,
914+ time_weights_dims, time_weights_scale, 0 ),
915+ CreateQuantizedBiasTensor (bias_data, bias_quantized, bias_dims,
916+ time_weights_scale, activation_state_scale),
917+ // Unlike TestIntegerSVDF() above, wire the *actual* (non-zero)
918+ // activation-state zero-point into the tensor here -- this is the
919+ // code path that exercises the bug.
920+ CreateQuantizedTensor (initial_activation_state_data,
921+ activation_state_quantized, activation_state_dims,
922+ activation_state_scale, activation_state_zero_point,
923+ /* is_variable=*/ true ),
924+ CreateQuantizedTensor (output_data, output_dims, output_scale,
925+ output_zero_point)};
926+
927+ tflite::Quantize (golden_output, golden_output_quantized,
928+ batch_size * num_units, output_scale, output_zero_point);
929+ tflite::Quantize (input_data, input_sequences_quantized,
930+ input_size * batch_size, input_scale, input_zero_point);
931+
932+ ValidateSVDFGoldens (batch_size, num_units, input_size, rank, tensors,
933+ tensor_count, kTfLiteActNone , input_sequences_quantized,
934+ input_size * batch_size, output_data,
935+ golden_output_quantized, /* tolerance=*/ 1 );
936+ }
937+
828938} // namespace
829939} // namespace testing
830940} // namespace tflite
@@ -961,4 +1071,11 @@ TEST(SvdfTest, SvdfQuantized1x16Input64x1OutputReluShouldMatchGoldenInt16) {
9611071 int16_t >();
9621072}
9631073
1074+ // Only reference kernels support full int8 svdf currently.
1075+ #if !defined(HEXAGON)
1076+ TEST (SvdfTest, SvdfInt8ActivationStateZeroPointOverflowRegressionTest) {
1077+ tflite::testing::SvdfInt8ActivationStateZeroPointOverflowRegressionTest ();
1078+ }
1079+ #endif
1080+
9641081TF_LITE_MICRO_TESTS_MAIN
0 commit comments