/*++ Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. Module Name: qlmul.cpp Abstract: This module implements routines to quantize linear mul. For quantization formula as specified in the ONNX operator documentation is: Output = Saturate(RoundToEven(Input / Scale) + ZeroPoint) --*/ #include "qladd.h" #if defined(MLAS_NEON64_INTRINSICS) template MLAS_FORCEINLINE static int16x8_t MlasExtendToS16Debias( typename SUI::i8x16_t Int8Vector, typename SUI::i8x8_t VectorBias ) { auto HalfVector = IsLow ? SUI::vget_low_i8(Int8Vector) : SUI::vget_high_i8(Int8Vector); return SUI::vreinterpretq_s16_i16(SUI::vsubl_i8(HalfVector, VectorBias)); } MLAS_FORCEINLINE static int16x8_t MlasQLinearMulVectorS16( int16x8_t va_s16x8, int16x8_t vb_s16x8, float32x4_t VectorScaleRatio, float32x4_t VectorZeroPointC ) { int32x4_t vacc0_lo = vmull_s16(vget_low_s16(va_s16x8), vget_low_s16(vb_s16x8)); int32x4_t vacc0_hi = vmull_s16(vget_high_s16(va_s16x8), vget_high_s16(vb_s16x8)); auto vacc0_lo_f32 = vaddq_f32(VectorZeroPointC, vmulq_f32(VectorScaleRatio, vcvtq_f32_s32(vacc0_lo))); auto vacc0_hi_f32 = vaddq_f32(VectorZeroPointC, vmulq_f32(VectorScaleRatio, vcvtq_f32_s32(vacc0_hi))); // using rounding to nearst, ties to even vacc0_lo = vcvtnq_s32_f32(vacc0_lo_f32); vacc0_hi = vcvtnq_s32_f32(vacc0_hi_f32); // Pack and saturate. return vcombine_s16(vqmovn_s32(vacc0_lo), vqmovn_s32(vacc0_hi)); } template static void MlasQLinearMulKernel( const DataType* InputA, float ScaleA, int32_t ZeroPointA, const DataType* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, DataType* OutputC, size_t N ) { typedef MLAS_SignedUnsignedIntOps SUI; const float32x4_t VectorScaleRatio = vmovq_n_f32(ScaleA * ScaleB / ScaleC); const typename SUI::i8x8_t VectorZeroPointA = SUI::vmov_n_i8((DataType)ZeroPointA); const typename SUI::i8x8_t VectorZeroPointB = SUI::vmov_n_i8((DataType)ZeroPointB); const float32x4_t VectorZeroPointC = vmovq_n_f32((float)ZeroPointC); typename SUI::T TailDataA[16] = { 0 }; typename SUI::T TailDataB[16] = { 0 }; int16x8_t vb0_s16x8, vb1_s16x8; if (IsScalarB) { const typename SUI::i8x8_t VectorB0 = SUI::vmov_n_i8(*InputB); vb0_s16x8 = SUI::vreinterpretq_s16_i16(SUI::vsubl_i8(VectorB0, VectorZeroPointB)); vb1_s16x8 = vb0_s16x8; } while (N > 0) { if (N < 16) { MlasCopyTailBytes((uint8_t*)TailDataA, (const uint8_t*)InputA, N); InputA = (const DataType*)TailDataA; if (!IsScalarB) { MlasCopyTailBytes((uint8_t*)TailDataB, (const uint8_t*)InputB, N); InputB = (const DataType*)TailDataB; } } const typename SUI::i8x16_t VectorA0 = SUI::vld1q_i8(InputA); InputA += 16; const int16x8_t va0_s16x8 = MlasExtendToS16Debias(VectorA0, VectorZeroPointA); const int16x8_t va1_s16x8 = MlasExtendToS16Debias(VectorA0, VectorZeroPointA);; if (!IsScalarB) { const typename SUI::i8x16_t VectorB0 = SUI::vld1q_i8(InputB); InputB += 16; vb0_s16x8 = MlasExtendToS16Debias(VectorB0, VectorZeroPointB); vb1_s16x8 = MlasExtendToS16Debias(VectorB0, VectorZeroPointB); } const int16x8_t vacc0 = MlasQLinearMulVectorS16(va0_s16x8, vb0_s16x8, VectorScaleRatio, VectorZeroPointC); const int16x8_t vacc1 = MlasQLinearMulVectorS16(va1_s16x8, vb1_s16x8, VectorScaleRatio, VectorZeroPointC); typename SUI::i8x16_t vc = SUI::combine_i8_s16(vacc0, vacc1); if (N >= 16) { N -= 16; SUI::vst1q_i8(OutputC, vc); OutputC += 16; } else { SUI::vst1q_i8(TailDataA, vc); MlasCopyTailBytes((uint8_t*)OutputC, (const uint8_t*)TailDataA, N); N = 0; } } } #elif defined(MLAS_SSE2_INTRINSICS) template MLAS_FORCEINLINE static __m128i MlasExtendToS16( __m128i Int8Vector, __m128i ZeroVector ); template <> MLAS_FORCEINLINE __m128i MlasExtendToS16( __m128i Int8Vector, __m128i ZeroVector ) { return _mm_unpacklo_epi8(Int8Vector, ZeroVector); } template <> MLAS_FORCEINLINE __m128i MlasExtendToS16( __m128i Int8Vector, __m128i ZeroVector ) { return _mm_unpackhi_epi8(Int8Vector, ZeroVector); } template <> MLAS_FORCEINLINE __m128i MlasExtendToS16( __m128i Int8Vector, __m128i ZeroVector ) { MLAS_UNREFERENCED_PARAMETER(ZeroVector); return _mm_srai_epi16(_mm_unpacklo_epi8(Int8Vector, Int8Vector), 8); } template <> MLAS_FORCEINLINE __m128i MlasExtendToS16( __m128i Int8Vector, __m128i ZeroVector ) { MLAS_UNREFERENCED_PARAMETER(ZeroVector); return _mm_srai_epi16(_mm_unpackhi_epi8(Int8Vector, Int8Vector), 8); } template MLAS_FORCEINLINE static __m128i MlasExtendToS16Debias( __m128i Int8Vector, __m128i ZeroVector, __m128i VectorBias ) { return _mm_sub_epi16(MlasExtendToS16(Int8Vector, ZeroVector), VectorBias); } MLAS_FORCEINLINE static __m128i MlasQLinearMulVectorS16( __m128i va_s16x8, __m128i vb_s16x8, __m128 VectorScaleRatio, __m128 VectorZeroPointC ) { const auto ab_lo = _mm_mullo_epi16(va_s16x8, vb_s16x8); const auto ab_hi = _mm_mulhi_epi16(va_s16x8, vb_s16x8); auto r_lo = _mm_unpacklo_epi16(ab_lo, ab_hi); auto r_hi = _mm_unpackhi_epi16(ab_lo, ab_hi); r_lo = _mm_cvtps_epi32(_mm_add_ps(_mm_mul_ps(_mm_cvtepi32_ps(r_lo), VectorScaleRatio), VectorZeroPointC)); r_hi = _mm_cvtps_epi32(_mm_add_ps(_mm_mul_ps(_mm_cvtepi32_ps(r_hi), VectorScaleRatio), VectorZeroPointC)); return _mm_packs_epi32(r_lo, r_hi); } template static void MlasQLinearMulKernel( const DataType* InputA, float ScaleA, int32_t ZeroPointA, const DataType* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, DataType* OutputC, size_t N ) { const auto VectorZeroPointA = _mm_set1_epi16((int16_t)ZeroPointA); const auto VectorZeroPointB = _mm_set1_epi16((int16_t)ZeroPointB); const auto VectorZeroPointC = MlasBroadcastFloat32x4((float)ZeroPointC); const auto VectorScaleRatio = MlasBroadcastFloat32x4(ScaleA * ScaleB / ScaleC); const auto ZeroVector = _mm_setzero_si128(); uint8_t TailDataA[16] = { 0 }; uint8_t TailDataB[16] = { 0 }; __m128i vb_lo_s16x8, vb_hi_s16x8; if (IsScalarB) { vb_lo_s16x8 = _mm_sub_epi16(_mm_set1_epi16((int16_t)*InputB), VectorZeroPointB); vb_hi_s16x8 = vb_lo_s16x8; } while (N > 0) { if (N < 16) { MlasCopyTailBytes(TailDataA, (const uint8_t*)InputA, N); InputA = (const DataType*)TailDataA; if (!IsScalarB) { MlasCopyTailBytes(TailDataB, (const uint8_t*)InputB, N); InputB = (const DataType*)TailDataB; } } const auto va_i8x16 = _mm_loadu_si128((const MLAS_INT32X4*)InputA); InputA += 16; const auto va_lo_s16x8 = MlasExtendToS16Debias(va_i8x16, ZeroVector, VectorZeroPointA); const auto va_hi_s16x8 = MlasExtendToS16Debias(va_i8x16, ZeroVector, VectorZeroPointA); if (!IsScalarB) { const auto vb_i8x16 = _mm_loadu_si128((const MLAS_INT32X4*)InputB); InputB += 16; vb_lo_s16x8 = MlasExtendToS16Debias(vb_i8x16, ZeroVector, VectorZeroPointB); vb_hi_s16x8 = MlasExtendToS16Debias(vb_i8x16, ZeroVector, VectorZeroPointB); } const auto vc_lo_s16x8 = MlasQLinearMulVectorS16(va_lo_s16x8, vb_lo_s16x8, VectorScaleRatio, VectorZeroPointC); const auto vc_hi_s16x8 = MlasQLinearMulVectorS16(va_hi_s16x8, vb_hi_s16x8, VectorScaleRatio, VectorZeroPointC); auto vc = MlasPackS16_128(vc_lo_s16x8, vc_hi_s16x8); if (N >= 16) { _mm_storeu_si128((__m128i*)OutputC, vc); OutputC += 16; N -= 16; } else { _mm_storeu_si128((__m128i*)TailDataA, vc); MlasCopyTailBytes((uint8_t*)OutputC, TailDataA, N); N = 0; } } } #elif defined(MLAS_VSX_INTRINSICS) template static void MlasQLinearMulKernel( const DataType* InputA, float ScaleA, int32_t ZeroPointA, const DataType* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, DataType* OutputC, size_t N ) { const float MinimumValue = (float)((int)std::numeric_limits::min() - ZeroPointC); const float MaximumValue = (float)((int)std::numeric_limits::max() - ZeroPointC); auto ZeroPointAVector = vec_splats(int32_t(ZeroPointA)); auto ZeroPointBVector = vec_splats(int32_t(ZeroPointB)); auto ZeroPointCVector = vec_splats(float(ZeroPointC)); auto ScaleAVector = vec_splats(ScaleA); auto ScaleBVector = vec_splats(ScaleB); auto ScaleCVector = vec_splats(ScaleC); auto MinimumVector = vec_splats(MinimumValue); auto MaximumVector = vec_splats(MaximumValue); float ValueB; __vector float ValueBVector; if (IsScalarB) { ValueB = ScaleB * (int32_t(InputB[0]) - ZeroPointB); ValueBVector = vec_splats(ValueB); } while (N >= 4) { __vector int32_t IntegerAVector {InputA[0], InputA[1], InputA[2], InputA[3]}; auto IntegerVector = vec_sub(IntegerAVector, ZeroPointAVector); auto ValueAVector = vec_mul(ScaleAVector, vec_ctf(IntegerVector, 0)); if (!IsScalarB) { __vector int32_t IntegerBVector {InputB[0], InputB[1], InputB[2], InputB[3]}; IntegerVector = vec_sub(IntegerBVector, ZeroPointBVector); ValueBVector = vec_mul(ScaleBVector, vec_ctf(IntegerVector, 0)); } auto ValueCVector = vec_div(vec_mul(ValueAVector, ValueBVector), ScaleCVector); ValueCVector = vec_min(vec_max(ValueCVector, MinimumVector), MaximumVector); ValueCVector = vec_nearbyint(vec_add(ValueCVector, ZeroPointCVector)); auto IntegerValueCVector = vec_signed(ValueCVector); OutputC[0] = (DataType) IntegerValueCVector[0]; OutputC[1] = (DataType) IntegerValueCVector[1]; OutputC[2] = (DataType) IntegerValueCVector[2]; OutputC[3] = (DataType) IntegerValueCVector[3]; OutputC += 4; InputA += 4; InputB += 4; N -= 4; // Suppress wrong GCC warnings MLAS_UNREFERENCED_PARAMETER(ValueAVector); } while (N > 0) { float ValueA = ScaleA * (int32_t(*InputA) - ZeroPointA); if (!IsScalarB) { ValueB = ScaleB * (int32_t(*InputB) - ZeroPointB); } float ValueC = (ValueA * ValueB) / ScaleC; ValueC = std::min(std::max(ValueC, MinimumValue), MaximumValue); *OutputC = (DataType)(int32_t)std::nearbyintf(ValueC + ZeroPointC); InputA++; InputB++; OutputC++; N--; } // Suppress wrong GCC warnings MLAS_UNREFERENCED_PARAMETER(ScaleAVector); MLAS_UNREFERENCED_PARAMETER(ScaleBVector); MLAS_UNREFERENCED_PARAMETER(ValueBVector); } #else // Pure C++ implementation. template static void MlasQLinearMulKernel( const DataType* InputA, float ScaleA, int32_t ZeroPointA, const DataType* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, DataType* OutputC, size_t N ) { const float MinimumValue = (float)((int)std::numeric_limits::min() - ZeroPointC); const float MaximumValue = (float)((int)std::numeric_limits::max() - ZeroPointC); float ValueB; if (IsScalarB) { ValueB = ScaleB * (int32_t(InputB[0]) - ZeroPointB); } for (size_t n = 0; n < N; n++) { float ValueA = ScaleA * (int32_t(InputA[n]) - ZeroPointA); if (!IsScalarB) { ValueB = ScaleB * (int32_t(InputB[n]) - ZeroPointB); } float ValueC = (ValueA * ValueB) / ScaleC; ValueC = std::min(std::max(ValueC, MinimumValue), MaximumValue); OutputC[n] = (DataType)(int32_t)std::nearbyintf(ValueC + ZeroPointC); } } #endif template void MLASCALL MlasQLinearMul( const DataType* InputA, float ScaleA, int32_t ZeroPointA, const DataType* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, DataType* OutputC, size_t N, bool IsScalarB ) { if (IsScalarB) { MlasQLinearMulKernel( InputA, ScaleA, ZeroPointA, InputB, ScaleB, ZeroPointB, ScaleC, ZeroPointC, OutputC, N); } else { MlasQLinearMulKernel( InputA, ScaleA, ZeroPointA, InputB, ScaleB, ZeroPointB, ScaleC, ZeroPointC, OutputC, N); } } // Explicit instantiation template void MlasQLinearMul( const uint8_t* InputA, float ScaleA, int32_t ZeroPointA, const uint8_t* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, uint8_t* OutputC, size_t N, bool IsScalarB ); template void MlasQLinearMul( const int8_t* InputA, float ScaleA, int32_t ZeroPointA, const int8_t* InputB, float ScaleB, int32_t ZeroPointB, float ScaleC, int32_t ZeroPointC, int8_t* OutputC, size_t N, bool IsScalarB );