blob: f3733278918646961568a3d23d8b1e2088ab6cff [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "TestHarness.h"
#include <android-base/logging.h>
#include <gmock/gmock-matchers.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <functional>
#include <limits>
#include <map>
#include <numeric>
#include <set>
#include <string>
#include <vector>
namespace test_helper {
namespace {
template <typename T>
constexpr bool nnIsFloat = std::is_floating_point_v<T> || std::is_same_v<T, _Float16>;
constexpr uint32_t kMaxNumberOfPrintedErrors = 10;
// TODO(b/139442217): Allow passing accuracy criteria from spec.
// Currently we only need relaxed accuracy criteria on mobilenet tests, so we return the quant8
// tolerance simply based on the current test name.
int getQuant8AllowedError() {
const ::testing::TestInfo* const testInfo =
::testing::UnitTest::GetInstance()->current_test_info();
const std::string testCaseName = testInfo->test_case_name();
const std::string testName = testInfo->name();
// We relax the quant8 precision for all tests with mobilenet:
// - CTS/VTS GeneratedTest and DynamicOutputShapeTest with mobilenet
// - VTS CompilationCachingTest and CompilationCachingSecurityTest except for TOCTOU tests
if (testName.find("mobilenet") != std::string::npos ||
(testCaseName.find("CompilationCaching") != std::string::npos &&
testName.find("TOCTOU") == std::string::npos)) {
return 3;
} else {
return 1;
}
}
uint32_t getNumberOfElements(const TestOperand& op) {
return std::reduce(op.dimensions.begin(), op.dimensions.end(), 1u, std::multiplies<uint32_t>());
}
// Check if the actual results meet the accuracy criterion.
template <typename T>
void expectNear(const TestOperand& op, const TestBuffer& result, const AccuracyCriterion& criterion,
bool allowInvalid = false) {
constexpr uint32_t kMinNumberOfElementsToTestBiasMSE = 10;
const T* actualBuffer = result.get<T>();
const T* expectedBuffer = op.data.get<T>();
uint32_t len = getNumberOfElements(op), numErrors = 0, numSkip = 0;
double bias = 0.0f, mse = 0.0f;
for (uint32_t i = 0; i < len; i++) {
// Compare all data types in double for precision and signed arithmetic.
double actual = static_cast<double>(actualBuffer[i]);
double expected = static_cast<double>(expectedBuffer[i]);
double tolerableRange = criterion.atol + criterion.rtol * std::fabs(expected);
EXPECT_FALSE(std::isnan(expected));
// Skip invalid floating point values.
if (allowInvalid &&
(std::isinf(expected) || (std::is_same_v<T, float> && std::fabs(expected) > 1e3) ||
(std::is_same_v<T, _Float16> || std::fabs(expected) > 1e2))) {
numSkip++;
continue;
}
// Accumulate bias and MSE. Use relative bias and MSE for floating point values.
double diff = actual - expected;
if constexpr (nnIsFloat<T>) {
diff /= std::max(1.0, std::abs(expected));
}
bias += diff;
mse += diff * diff;
// Print at most kMaxNumberOfPrintedErrors errors by EXPECT_NEAR.
if (numErrors < kMaxNumberOfPrintedErrors) {
EXPECT_NEAR(expected, actual, tolerableRange) << "When comparing element " << i;
}
if (std::fabs(actual - expected) > tolerableRange) numErrors++;
}
EXPECT_EQ(numErrors, 0u);
// Test bias and MSE.
if (len < numSkip + kMinNumberOfElementsToTestBiasMSE) return;
bias /= static_cast<double>(len - numSkip);
mse /= static_cast<double>(len - numSkip);
EXPECT_LE(std::fabs(bias), criterion.bias);
EXPECT_LE(mse, criterion.mse);
}
// For boolean values, we expect the number of mismatches does not exceed a certain ratio.
void expectBooleanNearlyEqual(const TestOperand& op, const TestBuffer& result,
float allowedErrorRatio) {
const bool8* actualBuffer = result.get<bool8>();
const bool8* expectedBuffer = op.data.get<bool8>();
uint32_t len = getNumberOfElements(op), numErrors = 0;
std::stringstream errorMsg;
for (uint32_t i = 0; i < len; i++) {
if (expectedBuffer[i] != actualBuffer[i]) {
if (numErrors < kMaxNumberOfPrintedErrors)
errorMsg << " Expected: " << expectedBuffer[i] << ", actual: " << actualBuffer[i]
<< ", when comparing element " << i << "\n";
numErrors++;
}
}
// When |len| is small, the allowedErrorCount will intentionally ceil at 1, which allows for
// greater tolerance.
uint32_t allowedErrorCount = static_cast<uint32_t>(std::ceil(allowedErrorRatio * len));
EXPECT_LE(numErrors, allowedErrorCount) << errorMsg.str();
}
// Calculates the expected probability from the unnormalized log-probability of
// each class in the input and compares it to the actual occurrence of that class
// in the output.
void expectMultinomialDistributionWithinTolerance(const TestModel& model,
const std::vector<TestBuffer>& buffers) {
// This function is only for RANDOM_MULTINOMIAL single-operation test.
CHECK_EQ(model.referenced.size(), 0u) << "Subgraphs not supported";
ASSERT_EQ(model.main.operations.size(), 1u);
ASSERT_EQ(model.main.operations[0].type, TestOperationType::RANDOM_MULTINOMIAL);
ASSERT_EQ(model.main.inputIndexes.size(), 1u);
ASSERT_EQ(model.main.outputIndexes.size(), 1u);
ASSERT_EQ(buffers.size(), 1u);
const auto& inputOperand = model.main.operands[model.main.inputIndexes[0]];
const auto& outputOperand = model.main.operands[model.main.outputIndexes[0]];
ASSERT_EQ(inputOperand.dimensions.size(), 2u);
ASSERT_EQ(outputOperand.dimensions.size(), 2u);
const int kBatchSize = inputOperand.dimensions[0];
const int kNumClasses = inputOperand.dimensions[1];
const int kNumSamples = outputOperand.dimensions[1];
const uint32_t outputLength = getNumberOfElements(outputOperand);
const int32_t* outputData = buffers[0].get<int32_t>();
std::vector<int> classCounts(kNumClasses);
for (uint32_t i = 0; i < outputLength; i++) {
classCounts[outputData[i]]++;
}
const uint32_t inputLength = getNumberOfElements(inputOperand);
std::vector<float> inputData(inputLength);
if (inputOperand.type == TestOperandType::TENSOR_FLOAT32) {
const float* inputRaw = inputOperand.data.get<float>();
std::copy(inputRaw, inputRaw + inputLength, inputData.begin());
} else if (inputOperand.type == TestOperandType::TENSOR_FLOAT16) {
const _Float16* inputRaw = inputOperand.data.get<_Float16>();
std::transform(inputRaw, inputRaw + inputLength, inputData.begin(),
[](_Float16 fp16) { return static_cast<float>(fp16); });
} else {
FAIL() << "Unknown input operand type for RANDOM_MULTINOMIAL.";
}
for (int b = 0; b < kBatchSize; ++b) {
float probabilitySum = 0;
const int batchIndex = kBatchSize * b;
for (int i = 0; i < kNumClasses; ++i) {
probabilitySum += expf(inputData[batchIndex + i]);
}
for (int i = 0; i < kNumClasses; ++i) {
float probability =
static_cast<float>(classCounts[i]) / static_cast<float>(kNumSamples);
float probabilityExpected = expf(inputData[batchIndex + i]) / probabilitySum;
EXPECT_THAT(probability,
::testing::FloatNear(probabilityExpected,
model.expectedMultinomialDistributionTolerance));
}
}
}
} // namespace
void checkResults(const TestModel& model, const std::vector<TestBuffer>& buffers,
const AccuracyCriteria& criteria) {
ASSERT_EQ(model.main.outputIndexes.size(), buffers.size());
for (uint32_t i = 0; i < model.main.outputIndexes.size(); i++) {
const uint32_t outputIndex = model.main.outputIndexes[i];
SCOPED_TRACE(testing::Message()
<< "When comparing output " << i << " (op" << outputIndex << ")");
const auto& operand = model.main.operands[outputIndex];
const auto& result = buffers[i];
if (operand.isIgnored) continue;
switch (operand.type) {
case TestOperandType::TENSOR_FLOAT32:
expectNear<float>(operand, result, criteria.float32, criteria.allowInvalidFpValues);
break;
case TestOperandType::TENSOR_FLOAT16:
expectNear<_Float16>(operand, result, criteria.float16,
criteria.allowInvalidFpValues);
break;
case TestOperandType::TENSOR_INT32:
case TestOperandType::INT32:
expectNear<int32_t>(operand, result, criteria.int32);
break;
case TestOperandType::TENSOR_QUANT8_ASYMM:
expectNear<uint8_t>(operand, result, criteria.quant8Asymm);
break;
case TestOperandType::TENSOR_QUANT8_SYMM:
expectNear<int8_t>(operand, result, criteria.quant8Symm);
break;
case TestOperandType::TENSOR_QUANT16_ASYMM:
expectNear<uint16_t>(operand, result, criteria.quant16Asymm);
break;
case TestOperandType::TENSOR_QUANT16_SYMM:
expectNear<int16_t>(operand, result, criteria.quant16Symm);
break;
case TestOperandType::TENSOR_BOOL8:
expectBooleanNearlyEqual(operand, result, criteria.bool8AllowedErrorRatio);
break;
case TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED:
expectNear<int8_t>(operand, result, criteria.quant8AsymmSigned);
break;
default:
FAIL() << "Data type not supported.";
}
}
}
void checkResults(const TestModel& model, const std::vector<TestBuffer>& buffers) {
// For RANDOM_MULTINOMIAL test only.
if (model.expectedMultinomialDistributionTolerance > 0.0f) {
expectMultinomialDistributionWithinTolerance(model, buffers);
return;
}
// Decide the default tolerable range.
//
// For floating-point models, we use the relaxed precision if either
// - relaxed computation flag is set
// - the model has at least one TENSOR_FLOAT16 operand
//
// The bias and MSE criteria are implicitly set to the maximum -- we do not enforce these
// criteria in normal generated tests.
//
// TODO: Adjust the error limit based on testing.
//
AccuracyCriteria criteria = {
// The relative tolerance is 5ULP of FP32.
.float32 = {.atol = 1e-5, .rtol = 5.0f * 1.1920928955078125e-7},
// Both the absolute and relative tolerance are 5ULP of FP16.
.float16 = {.atol = 5.0f * 0.0009765625, .rtol = 5.0f * 0.0009765625},
.int32 = {.atol = 1},
.quant8Asymm = {.atol = 1},
.quant8Symm = {.atol = 1},
.quant16Asymm = {.atol = 1},
.quant16Symm = {.atol = 1},
.bool8AllowedErrorRatio = 0.0f,
// Since generated tests are hand-calculated, there should be no invalid FP values.
.allowInvalidFpValues = false,
};
bool hasFloat16Inputs = false;
model.forEachSubgraph([&hasFloat16Inputs](const TestSubgraph& subgraph) {
if (!hasFloat16Inputs) {
hasFloat16Inputs = std::any_of(subgraph.operands.begin(), subgraph.operands.end(),
[](const TestOperand& op) {
return op.type == TestOperandType::TENSOR_FLOAT16;
});
}
});
if (model.isRelaxed || hasFloat16Inputs) {
criteria.float32 = criteria.float16;
}
const double quant8AllowedError = getQuant8AllowedError();
criteria.quant8Asymm.atol = quant8AllowedError;
criteria.quant8AsymmSigned.atol = quant8AllowedError;
criteria.quant8Symm.atol = quant8AllowedError;
checkResults(model, buffers, criteria);
}
TestModel convertQuant8AsymmOperandsToSigned(const TestModel& testModel) {
auto processSubgraph = [](TestSubgraph* subgraph) {
for (TestOperand& operand : subgraph->operands) {
if (operand.type == test_helper::TestOperandType::TENSOR_QUANT8_ASYMM) {
operand.type = test_helper::TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED;
operand.zeroPoint -= 128;
const uint8_t* inputOperandData = operand.data.get<uint8_t>();
int8_t* outputOperandData = operand.data.getMutable<int8_t>();
for (size_t i = 0; i < operand.data.size(); ++i) {
outputOperandData[i] =
static_cast<int8_t>(static_cast<int32_t>(inputOperandData[i]) - 128);
}
}
}
};
TestModel converted(testModel.copy());
processSubgraph(&converted.main);
for (TestSubgraph& subgraph : converted.referenced) {
processSubgraph(&subgraph);
}
return converted;
}
bool isQuantizedType(TestOperandType type) {
static const std::set<TestOperandType> kQuantizedTypes = {
TestOperandType::TENSOR_QUANT8_ASYMM,
TestOperandType::TENSOR_QUANT8_SYMM,
TestOperandType::TENSOR_QUANT16_ASYMM,
TestOperandType::TENSOR_QUANT16_SYMM,
TestOperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL,
TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED,
};
return kQuantizedTypes.count(type) > 0;
}
namespace {
const char* kOperationTypeNames[] = {
"ADD",
"AVERAGE_POOL_2D",
"CONCATENATION",
"CONV_2D",
"DEPTHWISE_CONV_2D",
"DEPTH_TO_SPACE",
"DEQUANTIZE",
"EMBEDDING_LOOKUP",
"FLOOR",
"FULLY_CONNECTED",
"HASHTABLE_LOOKUP",
"L2_NORMALIZATION",
"L2_POOL",
"LOCAL_RESPONSE_NORMALIZATION",
"LOGISTIC",
"LSH_PROJECTION",
"LSTM",
"MAX_POOL_2D",
"MUL",
"RELU",
"RELU1",
"RELU6",
"RESHAPE",
"RESIZE_BILINEAR",
"RNN",
"SOFTMAX",
"SPACE_TO_DEPTH",
"SVDF",
"TANH",
"BATCH_TO_SPACE_ND",
"DIV",
"MEAN",
"PAD",
"SPACE_TO_BATCH_ND",
"SQUEEZE",
"STRIDED_SLICE",
"SUB",
"TRANSPOSE",
"ABS",
"ARGMAX",
"ARGMIN",
"AXIS_ALIGNED_BBOX_TRANSFORM",
"BIDIRECTIONAL_SEQUENCE_LSTM",
"BIDIRECTIONAL_SEQUENCE_RNN",
"BOX_WITH_NMS_LIMIT",
"CAST",
"CHANNEL_SHUFFLE",
"DETECTION_POSTPROCESSING",
"EQUAL",
"EXP",
"EXPAND_DIMS",
"GATHER",
"GENERATE_PROPOSALS",
"GREATER",
"GREATER_EQUAL",
"GROUPED_CONV_2D",
"HEATMAP_MAX_KEYPOINT",
"INSTANCE_NORMALIZATION",
"LESS",
"LESS_EQUAL",
"LOG",
"LOGICAL_AND",
"LOGICAL_NOT",
"LOGICAL_OR",
"LOG_SOFTMAX",
"MAXIMUM",
"MINIMUM",
"NEG",
"NOT_EQUAL",
"PAD_V2",
"POW",
"PRELU",
"QUANTIZE",
"QUANTIZED_16BIT_LSTM",
"RANDOM_MULTINOMIAL",
"REDUCE_ALL",
"REDUCE_ANY",
"REDUCE_MAX",
"REDUCE_MIN",
"REDUCE_PROD",
"REDUCE_SUM",
"ROI_ALIGN",
"ROI_POOLING",
"RSQRT",
"SELECT",
"SIN",
"SLICE",
"SPLIT",
"SQRT",
"TILE",
"TOPK_V2",
"TRANSPOSE_CONV_2D",
"UNIDIRECTIONAL_SEQUENCE_LSTM",
"UNIDIRECTIONAL_SEQUENCE_RNN",
"RESIZE_NEAREST_NEIGHBOR",
"QUANTIZED_LSTM",
"IF",
"WHILE",
"ELU",
"HARD_SWISH",
"FILL",
"RANK",
};
const char* kOperandTypeNames[] = {
"FLOAT32",
"INT32",
"UINT32",
"TENSOR_FLOAT32",
"TENSOR_INT32",
"TENSOR_QUANT8_ASYMM",
"BOOL",
"TENSOR_QUANT16_SYMM",
"TENSOR_FLOAT16",
"TENSOR_BOOL8",
"FLOAT16",
"TENSOR_QUANT8_SYMM_PER_CHANNEL",
"TENSOR_QUANT16_ASYMM",
"TENSOR_QUANT8_SYMM",
"TENSOR_QUANT8_ASYMM_SIGNED",
};
bool isScalarType(TestOperandType type) {
static const std::vector<bool> kIsScalarOperandType = {
true, // TestOperandType::FLOAT32
true, // TestOperandType::INT32
true, // TestOperandType::UINT32
false, // TestOperandType::TENSOR_FLOAT32
false, // TestOperandType::TENSOR_INT32
false, // TestOperandType::TENSOR_QUANT8_ASYMM
true, // TestOperandType::BOOL
false, // TestOperandType::TENSOR_QUANT16_SYMM
false, // TestOperandType::TENSOR_FLOAT16
false, // TestOperandType::TENSOR_BOOL8
true, // TestOperandType::FLOAT16
false, // TestOperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL
false, // TestOperandType::TENSOR_QUANT16_ASYMM
false, // TestOperandType::TENSOR_QUANT8_SYMM
false, // TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED
};
return kIsScalarOperandType[static_cast<int>(type)];
}
std::string getOperandClassInSpecFile(TestOperandLifeTime lifetime) {
switch (lifetime) {
case TestOperandLifeTime::SUBGRAPH_INPUT:
return "Input";
case TestOperandLifeTime::SUBGRAPH_OUTPUT:
return "Output";
case TestOperandLifeTime::CONSTANT_COPY:
case TestOperandLifeTime::CONSTANT_REFERENCE:
case TestOperandLifeTime::NO_VALUE:
return "Parameter";
case TestOperandLifeTime::TEMPORARY_VARIABLE:
return "Internal";
default:
CHECK(false);
return "";
}
}
template <typename T>
const auto defaultToStringFunc = [](const T& value) { return std::to_string(value); };
template <>
const auto defaultToStringFunc<_Float16> =
[](const _Float16& value) { return std::to_string(static_cast<float>(value)); };
template <typename Iterator, class ToStringFunc>
std::string join(const std::string& joint, Iterator begin, Iterator end, ToStringFunc func) {
std::stringstream ss;
for (auto it = begin; it < end; it++) {
ss << (it == begin ? "" : joint) << func(*it);
}
return ss.str();
}
template <typename T, class ToStringFunc>
std::string join(const std::string& joint, const std::vector<T>& range, ToStringFunc func) {
return join(joint, range.begin(), range.end(), func);
}
template <typename T>
void dumpTestBufferToSpecFileHelper(const TestBuffer& buffer, std::ostream& os) {
const T* data = buffer.get<T>();
const uint32_t length = buffer.size() / sizeof(T);
os << "[" << join(", ", data, data + length, defaultToStringFunc<T>) << "]";
}
} // namespace
const char* toString(TestOperandType type) {
return kOperandTypeNames[static_cast<int>(type)];
}
const char* toString(TestOperationType type) {
return kOperationTypeNames[static_cast<int>(type)];
}
// Dump a test buffer.
void SpecDumper::dumpTestBuffer(TestOperandType type, const TestBuffer& buffer) {
switch (type) {
case TestOperandType::FLOAT32:
case TestOperandType::TENSOR_FLOAT32:
dumpTestBufferToSpecFileHelper<float>(buffer, mOs);
break;
case TestOperandType::INT32:
case TestOperandType::TENSOR_INT32:
dumpTestBufferToSpecFileHelper<int32_t>(buffer, mOs);
break;
case TestOperandType::TENSOR_QUANT8_ASYMM:
dumpTestBufferToSpecFileHelper<uint8_t>(buffer, mOs);
break;
case TestOperandType::TENSOR_QUANT8_SYMM:
case TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED:
dumpTestBufferToSpecFileHelper<int8_t>(buffer, mOs);
break;
case TestOperandType::TENSOR_QUANT16_ASYMM:
dumpTestBufferToSpecFileHelper<uint16_t>(buffer, mOs);
break;
case TestOperandType::TENSOR_QUANT16_SYMM:
dumpTestBufferToSpecFileHelper<int16_t>(buffer, mOs);
break;
case TestOperandType::BOOL:
case TestOperandType::TENSOR_BOOL8:
dumpTestBufferToSpecFileHelper<bool8>(buffer, mOs);
break;
case TestOperandType::FLOAT16:
case TestOperandType::TENSOR_FLOAT16:
dumpTestBufferToSpecFileHelper<_Float16>(buffer, mOs);
break;
default:
CHECK(false) << "Unknown type when dumping the buffer";
}
}
void SpecDumper::dumpTestOperand(const TestOperand& operand, uint32_t index) {
mOs << "op" << index << " = " << getOperandClassInSpecFile(operand.lifetime) << "(\"op" << index
<< "\", [\"" << toString(operand.type) << "\", ["
<< join(", ", operand.dimensions, defaultToStringFunc<uint32_t>) << "]";
if (operand.scale != 0.0f || operand.zeroPoint != 0) {
mOs << ", " << operand.scale << ", " << operand.zeroPoint;
}
mOs << "]";
if (operand.lifetime == TestOperandLifeTime::CONSTANT_COPY ||
operand.lifetime == TestOperandLifeTime::CONSTANT_REFERENCE) {
mOs << ", ";
dumpTestBuffer(operand.type, operand.data);
} else if (operand.lifetime == TestOperandLifeTime::NO_VALUE) {
mOs << ", value=None";
}
mOs << ")\n";
}
void SpecDumper::dumpTestOperation(const TestOperation& operation) {
auto toOperandName = [](uint32_t index) { return "op" + std::to_string(index); };
mOs << "model = model.Operation(\"" << toString(operation.type) << "\", "
<< join(", ", operation.inputs, toOperandName) << ").To("
<< join(", ", operation.outputs, toOperandName) << ")\n";
}
void SpecDumper::dumpTestModel() {
CHECK_EQ(kTestModel.referenced.size(), 0u) << "Subgraphs not supported";
// Dump model operands.
mOs << "# Model operands\n";
for (uint32_t i = 0; i < kTestModel.main.operands.size(); i++) {
dumpTestOperand(kTestModel.main.operands[i], i);
}
// Dump model operations.
mOs << "\n# Model operations\nmodel = Model()\n";
for (const auto& operation : kTestModel.main.operations) {
dumpTestOperation(operation);
}
// Dump input/output buffers.
mOs << "\n# Example\nExample({\n";
for (uint32_t i = 0; i < kTestModel.main.operands.size(); i++) {
const auto& operand = kTestModel.main.operands[i];
if (operand.lifetime != TestOperandLifeTime::SUBGRAPH_INPUT &&
operand.lifetime != TestOperandLifeTime::SUBGRAPH_OUTPUT) {
continue;
}
mOs << " op" << i << ": ";
dumpTestBuffer(operand.type, operand.data);
mOs << ",\n";
}
mOs << "}).DisableLifeTimeVariation()\n";
}
void SpecDumper::dumpResults(const std::string& name, const std::vector<TestBuffer>& results) {
CHECK_EQ(results.size(), kTestModel.main.outputIndexes.size());
mOs << "\n# Results from " << name << "\n{\n";
for (uint32_t i = 0; i < results.size(); i++) {
const uint32_t outputIndex = kTestModel.main.outputIndexes[i];
const auto& operand = kTestModel.main.operands[outputIndex];
mOs << " op" << outputIndex << ": ";
dumpTestBuffer(operand.type, results[i]);
mOs << ",\n";
}
mOs << "}\n";
}
template <typename T>
static TestOperand convertOperandToFloat32(const TestOperand& op) {
TestOperand converted = op;
converted.type =
isScalarType(op.type) ? TestOperandType::FLOAT32 : TestOperandType::TENSOR_FLOAT32;
converted.scale = 0.0f;
converted.zeroPoint = 0;
const uint32_t numberOfElements = getNumberOfElements(converted);
converted.data = TestBuffer(numberOfElements * sizeof(float));
const T* data = op.data.get<T>();
float* floatData = converted.data.getMutable<float>();
if (op.scale != 0.0f) {
std::transform(data, data + numberOfElements, floatData, [&op](T val) {
return (static_cast<float>(val) - op.zeroPoint) * op.scale;
});
} else {
std::transform(data, data + numberOfElements, floatData,
[](T val) { return static_cast<float>(val); });
}
return converted;
}
std::optional<TestModel> convertToFloat32Model(const TestModel& testModel) {
// Only single-operation graphs are supported.
if (testModel.referenced.size() > 0 || testModel.main.operations.size() > 1) {
return std::nullopt;
}
// Check for unsupported operations.
CHECK(!testModel.main.operations.empty());
const auto& operation = testModel.main.operations[0];
// Do not convert type-casting operations.
if (operation.type == TestOperationType::DEQUANTIZE ||
operation.type == TestOperationType::QUANTIZE ||
operation.type == TestOperationType::CAST) {
return std::nullopt;
}
// HASHTABLE_LOOKUP has different behavior in float and quant data types: float
// HASHTABLE_LOOKUP will output logical zero when there is a key miss, while quant
// HASHTABLE_LOOKUP will output byte zero.
if (operation.type == TestOperationType::HASHTABLE_LOOKUP) {
return std::nullopt;
}
auto convert = [&testModel, &operation](const TestOperand& op, uint32_t index) {
switch (op.type) {
case TestOperandType::TENSOR_FLOAT32:
case TestOperandType::FLOAT32:
case TestOperandType::TENSOR_BOOL8:
case TestOperandType::BOOL:
case TestOperandType::UINT32:
return op;
case TestOperandType::INT32:
// The third input of PAD_V2 uses INT32 to specify the padded value.
if (operation.type == TestOperationType::PAD_V2 && index == operation.inputs[2]) {
// The scale and zero point is inherited from the first input.
const uint32_t input0Index = operation.inputs[0];
const auto& input0 = testModel.main.operands[input0Index];
TestOperand scalarWithScaleAndZeroPoint = op;
scalarWithScaleAndZeroPoint.scale = input0.scale;
scalarWithScaleAndZeroPoint.zeroPoint = input0.zeroPoint;
return convertOperandToFloat32<int32_t>(scalarWithScaleAndZeroPoint);
}
return op;
case TestOperandType::TENSOR_INT32:
if (op.scale != 0.0f || op.zeroPoint != 0) {
return convertOperandToFloat32<int32_t>(op);
}
return op;
case TestOperandType::TENSOR_FLOAT16:
case TestOperandType::FLOAT16:
return convertOperandToFloat32<_Float16>(op);
case TestOperandType::TENSOR_QUANT8_ASYMM:
return convertOperandToFloat32<uint8_t>(op);
case TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED:
return convertOperandToFloat32<int8_t>(op);
case TestOperandType::TENSOR_QUANT16_ASYMM:
return convertOperandToFloat32<uint16_t>(op);
case TestOperandType::TENSOR_QUANT16_SYMM:
return convertOperandToFloat32<int16_t>(op);
default:
CHECK(false) << "OperandType not supported";
return TestOperand{};
}
};
TestModel converted = testModel;
for (uint32_t i = 0; i < testModel.main.operands.size(); i++) {
converted.main.operands[i] = convert(testModel.main.operands[i], i);
}
return converted;
}
template <typename T>
static void setDataFromFloat32Buffer(const TestBuffer& fpBuffer, TestOperand* op) {
const uint32_t numberOfElements = getNumberOfElements(*op);
const float* floatData = fpBuffer.get<float>();
T* data = op->data.getMutable<T>();
if (op->scale != 0.0f) {
std::transform(floatData, floatData + numberOfElements, data, [op](float val) {
int32_t unclamped = std::round(val / op->scale) + op->zeroPoint;
int32_t clamped = std::clamp<int32_t>(unclamped, std::numeric_limits<T>::min(),
std::numeric_limits<T>::max());
return static_cast<T>(clamped);
});
} else {
std::transform(floatData, floatData + numberOfElements, data,
[](float val) { return static_cast<T>(val); });
}
}
void setExpectedOutputsFromFloat32Results(const std::vector<TestBuffer>& results,
TestModel* model) {
CHECK_EQ(model->referenced.size(), 0u) << "Subgraphs not supported";
CHECK_EQ(model->main.operations.size(), 1u) << "Only single-operation graph is supported";
for (uint32_t i = 0; i < results.size(); i++) {
uint32_t outputIndex = model->main.outputIndexes[i];
auto& op = model->main.operands[outputIndex];
switch (op.type) {
case TestOperandType::TENSOR_FLOAT32:
case TestOperandType::FLOAT32:
case TestOperandType::TENSOR_BOOL8:
case TestOperandType::BOOL:
case TestOperandType::INT32:
case TestOperandType::UINT32:
op.data = results[i];
break;
case TestOperandType::TENSOR_INT32:
if (op.scale != 0.0f) {
setDataFromFloat32Buffer<int32_t>(results[i], &op);
} else {
op.data = results[i];
}
break;
case TestOperandType::TENSOR_FLOAT16:
case TestOperandType::FLOAT16:
setDataFromFloat32Buffer<_Float16>(results[i], &op);
break;
case TestOperandType::TENSOR_QUANT8_ASYMM:
setDataFromFloat32Buffer<uint8_t>(results[i], &op);
break;
case TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED:
setDataFromFloat32Buffer<int8_t>(results[i], &op);
break;
case TestOperandType::TENSOR_QUANT16_ASYMM:
setDataFromFloat32Buffer<uint16_t>(results[i], &op);
break;
case TestOperandType::TENSOR_QUANT16_SYMM:
setDataFromFloat32Buffer<int16_t>(results[i], &op);
break;
default:
CHECK(false) << "OperandType not supported";
}
}
}
} // namespace test_helper