/* * Copyright (c) Facebook, Inc. and its affiliates. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include class DWConvMicrokernelTester { public: inline DWConvMicrokernelTester& width(uint32_t width) { assert(width >= 1); this->width_ = width; return *this; } inline uint32_t width() const { return this->width_; } inline DWConvMicrokernelTester& subsampling(uint32_t subsampling) { assert(subsampling >= 1); this->subsampling_ = subsampling; return *this; } inline uint32_t subsampling() const { return this->subsampling_; } inline DWConvMicrokernelTester& channels(uint32_t channels) { assert(channels >= 1); this->channels_ = channels; return *this; } inline uint32_t channels() const { return this->channels_; } inline DWConvMicrokernelTester& cr(uint32_t cr) { assert(cr != 0); assert((cr & (cr - 1)) == 0); this->cr_ = cr; return *this; } inline uint32_t cr() const { return this->cr_; } inline uint32_t packedChannels() const { return (channels() + (cr() - 1)) & -cr(); } inline DWConvMicrokernelTester& kernelHeight(uint32_t kernelHeight) { assert(kernelHeight != 0); this->kernelHeight_ = kernelHeight; return *this; } inline uint32_t kernelHeight() const { return this->kernelHeight_; } inline DWConvMicrokernelTester& kernelWidth(uint32_t kernelWidth) { assert(kernelWidth != 0); this->kernelWidth_ = kernelWidth; return *this; } inline uint32_t kernelWidth() const { return this->kernelWidth_; } inline uint32_t kernelSize() const { return kernelHeight() * kernelWidth(); } inline DWConvMicrokernelTester& inputStride(uint32_t inputStride) { assert(inputStride != 0); this->inputStride_ = inputStride; return *this; } inline uint32_t inputStride() const { if (this->inputStride_ == 0) { return channels(); } else { assert(this->inputStride_ >= channels()); return this->inputStride_; } } inline DWConvMicrokernelTester& outputStride(uint32_t outputStride) { assert(outputStride != 0); this->outputStride_ = outputStride; return *this; } inline uint32_t outputStride() const { if (this->outputStride_ == 0) { return channels(); } else { assert(this->outputStride_ >= channels()); return this->outputStride_; } } inline DWConvMicrokernelTester& inputZeroPoint(uint8_t inputZeroPoint) { this->inputZeroPoint_ = inputZeroPoint; return *this; } inline uint8_t inputZeroPoint() const { return this->inputZeroPoint_; } inline DWConvMicrokernelTester& kernelZeroPoint(uint8_t kernelZeroPoint) { this->kernelZeroPoint_ = kernelZeroPoint; return *this; } inline uint8_t kernelZeroPoint() const { return this->kernelZeroPoint_; } inline DWConvMicrokernelTester& qmin(uint8_t qmin) { this->qmin_ = qmin; return *this; } inline uint8_t qmin() const { return this->qmin_; } inline DWConvMicrokernelTester& qmax(uint8_t qmax) { this->qmax_ = qmax; return *this; } inline uint8_t qmax() const { return this->qmax_; } inline DWConvMicrokernelTester& iterations(size_t iterations) { this->iterations_ = iterations; return *this; } inline size_t iterations() const { return this->iterations_; } void test( pytorch_q8dwconv2d_up_ukernel_function q8dwconv, bool per_channel = false) const { std::random_device randomDevice; auto rng = std::mt19937(randomDevice()); auto s32rng = std::bind(std::uniform_int_distribution(-10000, 10000), rng); auto u8rng = std::bind(std::uniform_int_distribution(), rng); std::vector input( (kernelSize() + (width() * subsampling() - 1) * kernelHeight() - 1) * inputStride() + channels() + 8); std::vector kernel(channels() * kernelSize()); std::vector> packedWeights( (kernelSize() + sizeof(int32_t) / sizeof(uint8_t)) * packedChannels()); std::vector bias(packedChannels()); std::vector accumulators(width() * channels()); std::vector output((width() - 1) * outputStride() + channels()); std::vector indirectInput( kernelSize() + (width() * subsampling() - 1) * kernelHeight()); const uint8_t* inputPtr = input.data() + 8; for (size_t iteration = 0; iteration < iterations(); iteration++) { std::generate(input.begin(), input.end(), std::ref(u8rng)); std::generate(kernel.begin(), kernel.end(), std::ref(u8rng)); std::generate(bias.begin(), bias.end(), std::ref(s32rng)); std::fill(accumulators.begin(), accumulators.end(), 0); ASSERT_NE( *std::max_element(input.cbegin(), input.cend()), *std::min_element(input.cbegin(), input.cend())); ASSERT_NE( *std::max_element(kernel.cbegin(), kernel.cend()), *std::min_element(kernel.cbegin(), kernel.cend())); std::fill(packedWeights.begin(), packedWeights.end(), 0xA5); size_t num_zero_points_padded = channels() + 8; std::vector kernel_zero_points( num_zero_points_padded, 0); if (per_channel) { std::generate( kernel_zero_points.begin(), kernel_zero_points.begin() + channels(), std::ref(u8rng)); } pytorch_pack_q8dw_w( kernelHeight(), kernelWidth(), channels(), cr(), #if !PYTORCH_QNNPACK_RUNTIME_QUANTIZATION inputZeroPoint(), kernel_zero_points.data(), #endif kernel.data(), bias.data(), packedWeights.data()); for (size_t i = 0; i < kernelSize() + (width() * subsampling() - 1) * kernelHeight(); i++) { indirectInput[i] = inputPtr + i * inputStride(); } std::shuffle(indirectInput.begin(), indirectInput.end(), rng); for (size_t x = 0; x < width(); x++) { for (size_t c = 0; c < channels(); c++) { int32_t acc = bias[c]; for (size_t kx = 0; kx < kernelWidth(); kx++) { for (size_t ky = 0; ky < kernelHeight(); ky++) { acc += (int32_t(indirectInput [(x * subsampling() + kx) * kernelHeight() + ky][c]) - int32_t(inputZeroPoint())) * (int32_t( kernel[(c * kernelHeight() + ky) * kernelWidth() + kx]) - int32_t(kernel_zero_points[c])); } } accumulators[x * channels() + c] = acc; } } const int32_t accumulatorsMin = *std::min_element(accumulators.cbegin(), accumulators.cend()); const int32_t accumulatorsMax = *std::max_element(accumulators.cbegin(), accumulators.cend()); const uint32_t accumulatorsRange = uint32_t(accumulatorsMax) - uint32_t(accumulatorsMin); ASSERT_NE(0, accumulatorsRange); const double outputScale = accumulatorsRange >= 256 ? double(accumulatorsRange) / 255.0 : 1.00001; const uint8_t outputZeroPoint = uint8_t(std::max( std::min( lrint( 127.5 - 0.5 * double(accumulatorsMin + accumulatorsMax) / outputScale), long(std::numeric_limits::max())), long(std::numeric_limits::min()))); std::vector requantization_scales(num_zero_points_padded, 1.0f / float(outputScale)); if (per_channel) { auto f32rng = std::bind(std::uniform_real_distribution(1, 5), rng); auto scale_generator = [&]() -> float {return (f32rng()/outputScale);}; std::generate( requantization_scales.begin(), requantization_scales.end(), std::ref(scale_generator)); } const union pytorch_qnnp_conv_quantization_params quantizationParams = pytorch_qnnp_compute_conv_quantization_params( inputZeroPoint(), kernel_zero_points.data(), requantization_scales.data(), outputZeroPoint, qmin(), qmax()); const union pytorch_qnnp_fp32_requantization_params scalarRequantizationParams = pytorch_qnnp_compute_scalar_fp32_requantization_params( requantization_scales.data(), outputZeroPoint, qmin(), qmax()); q8dwconv( channels(), width(), indirectInput.data(), packedWeights.data(), output.data(), kernelHeight() * subsampling() * sizeof(void*), (outputStride() - channels()) * sizeof(uint8_t), &quantizationParams); for (size_t x = 0; x < width(); x++) { for (size_t c = 0; c < channels(); c++) { #if defined(__arm__) || defined(_M_ARM) const uint8_t referenceOutput = pytorch_qnnp_fp32_requantize_magic( accumulators[x * channels() + c], scalarRequantizationParams, c); #else const uint8_t referenceOutput = pytorch_qnnp_fp32_requantize( accumulators[x * channels() + c], scalarRequantizationParams, c); #endif const double scaledAccumulator = accumulators[x * channels() + c] * requantization_scales[c] + double(outputZeroPoint); const double clampedAccumulator = std::max( std::min(scaledAccumulator, double(qmax())), double(qmin())); ASSERT_NEAR( clampedAccumulator, double(output[x * outputStride() + c]), 0.6) << "x = " << x << ", channel = " << c; ASSERT_EQ( uint32_t(referenceOutput), uint32_t(output[x * outputStride() + c])) << "x = " << x << ", channel = " << c; } } } } void test( pytorch_q8dwconv2d_mp_ukernel_function q8dwconv, bool per_channel = false) const { ASSERT_EQ(25, kernelSize()) << "only 5x5 microkernel is currently supported"; std::random_device randomDevice; auto rng = std::mt19937(randomDevice()); auto s32rng = std::bind(std::uniform_int_distribution(-10000, 10000), rng); auto u8rng = std::bind(std::uniform_int_distribution(), rng); std::vector input( (kernelSize() + (width() * subsampling() - 1) * kernelHeight() - 1) * inputStride() + channels() + 8); std::vector kernel(channels() * kernelSize()); std::vector> packedWeights( (kernelSize() + sizeof(int32_t) / sizeof(uint8_t)) * packedChannels()); std::vector bias(packedChannels()); std::vector accumulators(width() * channels()); std::vector mpAcc(width() * packedChannels()); std::vector output((width() - 1) * outputStride() + channels()); std::vector indirectInput( kernelSize() + (width() * subsampling() - 1) * kernelHeight()); const uint8_t* inputPtr = input.data() + 8; for (size_t iteration = 0; iteration < iterations(); iteration++) { std::generate(input.begin(), input.end(), std::ref(u8rng)); std::generate(kernel.begin(), kernel.end(), std::ref(u8rng)); std::generate(bias.begin(), bias.end(), std::ref(s32rng)); std::fill(accumulators.begin(), accumulators.end(), 0); std::fill(mpAcc.begin(), mpAcc.end(), 0xA5A55A5A); ASSERT_NE( *std::max_element(input.cbegin(), input.cend()), *std::min_element(input.cbegin(), input.cend())); ASSERT_NE( *std::max_element(kernel.cbegin(), kernel.cend()), *std::min_element(kernel.cbegin(), kernel.cend())); std::fill(packedWeights.begin(), packedWeights.end(), 0xA5); size_t num_zero_points_padded = channels() + 8; std::vector kernel_zero_points(num_zero_points_padded, this->kernelZeroPoint_); if (per_channel) { std::generate( kernel_zero_points.begin(), kernel_zero_points.end(), std::ref(u8rng)); } ASSERT_EQ(25, kernelSize()) << "only 5x5 microkernel is currently supported"; pytorch_pack_q8dw_2d_w_dilation( kernelHeight(), kernelWidth(), channels(), cr(), 0, kernelHeight(), 0, 2, kernel.data(), bias.data(), packedWeights.data(), true); pytorch_pack_q8dw_2d_w_dilation( kernelHeight(), kernelWidth(), channels(), cr(), 0, kernelHeight(), 2, 4, kernel.data(), bias.data(), packedWeights.data() + (10 + sizeof(int32_t) / sizeof(uint8_t)) * packedChannels(), false); pytorch_pack_q8dw_2d_w_dilation( kernelHeight(), kernelWidth(), channels(), cr(), 0, kernelHeight(), 4, 5, kernel.data(), bias.data(), packedWeights.data() + (20 + sizeof(int32_t) / sizeof(uint8_t)) * packedChannels(), false); for (size_t i = 0; i < kernelSize() + (width() * subsampling() - 1) * kernelHeight(); i++) { indirectInput[i] = inputPtr + i * inputStride(); } std::shuffle(indirectInput.begin(), indirectInput.end(), rng); for (size_t x = 0; x < width(); x++) { for (size_t c = 0; c < channels(); c++) { int32_t acc = bias[c]; for (size_t kx = 0; kx < kernelWidth(); kx++) { for (size_t ky = 0; ky < kernelHeight(); ky++) { acc += (int32_t(indirectInput [(x * subsampling() + kx) * kernelHeight() + ky][c]) - int32_t(inputZeroPoint())) * (int32_t( kernel[(c * kernelHeight() + ky) * kernelWidth() + kx]) - int32_t(kernel_zero_points[c])); } } accumulators[x * channels() + c] = acc; } } const int32_t accumulatorsMin = *std::min_element(accumulators.cbegin(), accumulators.cend()); const int32_t accumulatorsMax = *std::max_element(accumulators.cbegin(), accumulators.cend()); const uint32_t accumulatorsRange = uint32_t(accumulatorsMax) - uint32_t(accumulatorsMin); ASSERT_NE(0, accumulatorsRange); const double outputScale = accumulatorsRange >= 256 ? double(accumulatorsRange) / 255.0 : 1.00001; const uint8_t outputZeroPoint = uint8_t(std::max( std::min( lrint( 127.5 - 0.5 * double(accumulatorsMin + accumulatorsMax) / outputScale), long(std::numeric_limits::max())), long(std::numeric_limits::min()))); std::vector requantization_scales(num_zero_points_padded, 1.0f / float(outputScale)); if (per_channel) { auto f32rng = std::bind(std::uniform_real_distribution(1, 5), rng); auto scale_generator = [&]() -> float {return (f32rng()/outputScale);}; std::generate( requantization_scales.begin(), requantization_scales.end(), std::ref(scale_generator)); } const union pytorch_qnnp_conv_quantization_params quantizationParams = pytorch_qnnp_compute_conv_quantization_params( inputZeroPoint(), kernel_zero_points.data(), requantization_scales.data(), outputZeroPoint, qmin(), qmax()); const union pytorch_qnnp_fp32_requantization_params scalarRequantizationParams = pytorch_qnnp_compute_scalar_fp32_requantization_params( requantization_scales.data(), outputZeroPoint, qmin(), qmax()); q8dwconv( channels(), width(), indirectInput.data(), packedWeights.data(), mpAcc.data(), output.data(), kernelHeight() * subsampling() * sizeof(void*), (outputStride() - channels()) * sizeof(uint8_t), &quantizationParams); for (size_t x = 0; x < width(); x++) { for (size_t c = 0; c < channels(); c++) { #if defined(__arm__) || defined(_M_ARM) const uint8_t referenceOutput = pytorch_qnnp_fp32_requantize_magic( accumulators[x * channels() + c], scalarRequantizationParams, c); #else const uint8_t referenceOutput = pytorch_qnnp_fp32_requantize( accumulators[x * channels() + c], scalarRequantizationParams, c); #endif const double scaledAccumulator = accumulators[x * channels() + c] * requantization_scales[c] + double(outputZeroPoint); const double clampedAccumulator = std::max( std::min(scaledAccumulator, double(qmax())), double(qmin())); ASSERT_NEAR( clampedAccumulator, double(output[x * outputStride() + c]), 0.6) << "x = " << x << ", channel = " << c; ASSERT_EQ( uint32_t(referenceOutput), uint32_t(output[x * outputStride() + c])) << "x = " << x << ", channel = " << c; } } } } private: uint32_t channels_{1}; uint32_t cr_{1}; uint32_t width_{1}; uint32_t subsampling_{1}; uint32_t kernelHeight_{1}; uint32_t kernelWidth_{1}; uint32_t inputStride_{0}; uint32_t outputStride_{0}; uint8_t inputZeroPoint_{127}; uint8_t kernelZeroPoint_{127}; uint8_t qmin_{0}; uint8_t qmax_{255}; size_t iterations_{3}; };