blob: e014ae993ae1825312b2456c263a312230bd0ff4 [file] [edit]
/*
* Copyright 2024 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <fstream>
#include "absl/strings/str_cat.h"
#include "common/single_op_model_builder.h"
#include "common/test_util/stable_delegate_env.h"
#include "tensorflow/lite/core/c/builtin_op_data.h"
#include "tensorflow/lite/core/c/c_api.h"
#include "tensorflow/lite/interpreter.h"
#include "tensorflow/lite/interpreter_builder.h"
#include "tensorflow/lite/kernels/register.h"
#include "tensorflow/lite/tools/utils.h"
StableDelegateEnvironment* g_env = nullptr;
namespace tflite::cros::tests {
struct TestConfig {
std::vector<Flag> GetFlags() {
std::vector<Flag> flags = {
Flag::CreateFlag(
"dump_model_dir", &dump_model_dir,
"Where to save the test models. Empty means do not save at all."),
};
return flags;
}
std::string dump_model_dir = "";
} g_config;
size_t NumElements(const TfLiteTensor* tensor) {
size_t n = 1;
for (int i = 0; i < tensor->dims->size; i++) {
n *= tensor->dims->data[i];
}
return n;
}
template <typename T>
std::vector<double> CopyDataAsDoubleVector(const T* src, size_t n) {
// Note that we implicitly convert the values from T to double here.
return std::vector<double>(src, src + n);
}
std::vector<double> CopyTensorAsDoubleVector(const TfLiteTensor* tensor) {
int n = NumElements(tensor);
switch (tensor->type) {
case kTfLiteFloat32:
return CopyDataAsDoubleVector(tensor->data.f, n);
case kTfLiteFloat64:
return CopyDataAsDoubleVector(tensor->data.f64, n);
case kTfLiteInt8:
return CopyDataAsDoubleVector(tensor->data.int8, n);
case kTfLiteUInt8:
return CopyDataAsDoubleVector(tensor->data.uint8, n);
case kTfLiteInt16:
return CopyDataAsDoubleVector(tensor->data.i16, n);
case kTfLiteUInt16:
return CopyDataAsDoubleVector(tensor->data.ui16, n);
case kTfLiteInt32:
return CopyDataAsDoubleVector(tensor->data.i32, n);
case kTfLiteUInt32:
return CopyDataAsDoubleVector(tensor->data.u32, n);
case kTfLiteBool:
return CopyDataAsDoubleVector(tensor->data.b, n);
default:
ADD_FAILURE() << "Unexpected tensor type "
<< TfLiteTypeGetName(tensor->type);
return {};
}
}
bool IsFullyDelegated(Interpreter* interpreter) {
for (int index : interpreter->execution_plan()) {
TfLiteNode node = interpreter->node_and_registration(index)->first;
if (node.delegate == nullptr) {
return false;
}
}
return true;
}
using ::testing::DoubleNear;
using ::testing::Pointwise;
// A relaxed tolerance when comparing the computation results since delegates
// may internally use fp16 for fp32 arithmetic, and this test is mainly used for
// checking operator compatibility.
constexpr float kEps = 1e-2;
class ModelTest : public testing::Test {
protected:
void SetRandomInputRange(float low, float high) {
random_input_range_ = std::make_pair(low, high);
}
void UsePositiveInput() {
// A handle chosen positive random range that should work for most
// operators. Note that we exclude near zero positive values intentionally
// to avoid potential numerical issues like log(~0).
SetRandomInputRange(1, 10);
}
void TestModel(const SingleOpModelBuilder& model_builder) {
if (!g_config.dump_model_dir.empty()) {
DumpModel(model_builder.Build());
}
// Call GetDelegate() instead of CreateDelegate() here to reuse the delegate
// instance and reduce the overhead of creating/destorying the delegate
// itself.
auto delegate = g_env->GetDelegate();
ASSERT_NE(delegate, nullptr);
// Run model with/without the delegate and compare the result.
std::vector<std::vector<double>> actual, expected;
random_input_data_.clear();
RunModel(model_builder.Build(), delegate, actual);
RunModel(model_builder.Build(), nullptr, expected);
ASSERT_EQ(actual.size(), expected.size());
for (size_t i = 0; i < actual.size(); i++) {
EXPECT_THAT(actual[i], Pointwise(DoubleNear(kEps), expected[i]));
}
}
private:
void RunModel(std::unique_ptr<FlatBufferModel> model,
TfLiteDelegate* delegate,
std::vector<std::vector<double>>& output_data) {
ASSERT_NE(model, nullptr);
// Build interpreter.
ops::builtin::BuiltinOpResolverWithoutDefaultDelegates resolver;
InterpreterBuilder builder(*model, resolver);
if (delegate != nullptr) {
builder.AddDelegate(delegate);
}
std::unique_ptr<Interpreter> interpreter;
ASSERT_EQ(builder(&interpreter), kTfLiteOk);
// Allocate tensors and fill inputs with random data.
ASSERT_EQ(interpreter->AllocateTensors(), kTfLiteOk);
for (int input : interpreter->inputs()) {
TfLiteTensor* tensor = interpreter->tensor(input);
if (tensor->allocation_type == kTfLiteMmapRo) {
continue;
}
// Reuse the same random input from the previous run if exists.
utils::InputTensorData& data = random_input_data_[input];
if (data.data == nullptr) {
float low = 0, high = 0;
if (random_input_range_.has_value()) {
low = random_input_range_->first;
high = random_input_range_->second;
} else {
utils::GetDataRangesForType(tensor->type, &low, &high);
}
data = utils::CreateRandomTensorData(*tensor, low, high);
}
ASSERT_EQ(data.bytes, tensor->bytes);
memcpy(tensor->data.data, data.data.get(), data.bytes);
}
// Run the inference and check the execution is fully delegated.
ASSERT_EQ(interpreter->Invoke(), kTfLiteOk);
if (delegate != nullptr) {
EXPECT_TRUE(IsFullyDelegated(interpreter.get()));
}
// Copy the output data.
size_t num_out = interpreter->outputs().size();
output_data.resize(num_out);
for (size_t i = 0; i < num_out; i++) {
output_data[i] = CopyTensorAsDoubleVector(interpreter->output_tensor(i));
}
}
void DumpModel(std::unique_ptr<FlatBufferModel> model) {
ASSERT_FALSE(g_config.dump_model_dir.empty());
const testing::TestInfo* info =
testing::UnitTest::GetInstance()->current_test_info();
// Save into {dir}/{suite}.{test}-{counter}.tflite.
std::string path =
absl::StrCat(g_config.dump_model_dir, "/", info->test_suite_name(), ".",
info->name(), "-", dumped_model_counter_, ".tflite");
std::ofstream out(path, std::ios::out | std::ios::binary);
const Allocation* alloc = model->allocation();
out.write(static_cast<const char*>(alloc->base()), alloc->bytes());
LOGF(INFO) << "Save to " << path;
dumped_model_counter_++;
}
std::map<int, utils::InputTensorData> random_input_data_;
std::optional<std::pair<float, float>> random_input_range_;
int dumped_model_counter_ = 0;
};
class OpTest : public ModelTest {
protected:
template <typename T = std::monostate>
void TestUnaryOp(TfLiteBuiltinOperator op, TfLiteType type, T params = {}) {
TestOp(op, type, params, /*num_inputs=*/1);
}
template <typename T = std::monostate>
void TestBinaryOp(TfLiteBuiltinOperator op, TfLiteType type, T params = {}) {
TestOp(op, type, params, /*num_inputs=*/2);
}
private:
template <typename T>
void TestOp(TfLiteBuiltinOperator op,
TfLiteType type,
T params,
int num_inputs) {
// TODO(shik): Parameterize tensor ranks and make it controllable as
// command-line flags. For now, test only 4D which should be supported
// universally by all delegates we care about.
const std::vector<int> shape = {1, 2, 3, 4};
SingleOpModelBuilder builder;
for (int i = 0; i < num_inputs; i++) {
builder.AddInput(type, shape);
}
TestModel(builder.AddOutput(type, shape).AddOperator<T>(op, params));
}
};
TEST_F(OpTest, Abs) {
TestUnaryOp(kTfLiteBuiltinAbs, kTfLiteFloat32);
}
TEST_F(OpTest, Cos) {
TestUnaryOp(kTfLiteBuiltinCos, kTfLiteFloat32);
}
TEST_F(OpTest, Elu) {
TestUnaryOp(kTfLiteBuiltinElu, kTfLiteFloat32);
}
TEST_F(OpTest, Exp) {
TestUnaryOp(kTfLiteBuiltinExp, kTfLiteFloat32);
}
TEST_F(OpTest, Floor) {
TestUnaryOp(kTfLiteBuiltinFloor, kTfLiteFloat32);
}
TEST_F(OpTest, Gelu) {
TestUnaryOp<TfLiteGeluParams>(kTfLiteBuiltinGelu, kTfLiteFloat32,
{.approximate = false});
TestUnaryOp<TfLiteGeluParams>(kTfLiteBuiltinGelu, kTfLiteFloat32,
{.approximate = true});
}
TEST_F(OpTest, LeakyRelu) {
TestUnaryOp<TfLiteLeakyReluParams>(kTfLiteBuiltinLeakyRelu, kTfLiteFloat32,
{.alpha = 0.8});
}
TEST_F(OpTest, Log) {
UsePositiveInput();
TestUnaryOp(kTfLiteBuiltinLog, kTfLiteFloat32);
}
TEST_F(OpTest, Logistic) {
TestUnaryOp(kTfLiteBuiltinLogistic, kTfLiteFloat32);
}
TEST_F(OpTest, Neg) {
TestUnaryOp(kTfLiteBuiltinNeg, kTfLiteFloat32);
}
TEST_F(OpTest, Relu) {
TestUnaryOp(kTfLiteBuiltinRelu, kTfLiteFloat32);
}
TEST_F(OpTest, Relu6) {
SetRandomInputRange(-1, 7);
TestUnaryOp(kTfLiteBuiltinRelu6, kTfLiteFloat32);
}
TEST_F(OpTest, ReluN1To1) {
SetRandomInputRange(-2, 2);
TestUnaryOp(kTfLiteBuiltinReluN1To1, kTfLiteFloat32);
}
TEST_F(OpTest, Rsqrt) {
UsePositiveInput();
TestUnaryOp(kTfLiteBuiltinRsqrt, kTfLiteFloat32);
}
TEST_F(OpTest, Sign) {
TestUnaryOp(kTfLiteBuiltinSign, kTfLiteFloat32);
}
TEST_F(OpTest, Sin) {
TestUnaryOp(kTfLiteBuiltinSin, kTfLiteFloat32);
}
TEST_F(OpTest, Sqrt) {
UsePositiveInput();
TestUnaryOp(kTfLiteBuiltinSqrt, kTfLiteFloat32);
}
TEST_F(OpTest, Square) {
TestUnaryOp(kTfLiteBuiltinSquare, kTfLiteFloat32);
}
TEST_F(OpTest, Tanh) {
TestUnaryOp(kTfLiteBuiltinTanh, kTfLiteFloat32);
}
TEST_F(OpTest, Add) {
TestBinaryOp<TfLiteAddParams>(kTfLiteBuiltinAdd, kTfLiteFloat32);
}
TEST_F(OpTest, Div) {
UsePositiveInput();
TestBinaryOp<TfLiteDivParams>(kTfLiteBuiltinDiv, kTfLiteFloat32);
}
TEST_F(OpTest, FloorDiv) {
UsePositiveInput();
TestBinaryOp(kTfLiteBuiltinFloorDiv, kTfLiteFloat32);
}
TEST_F(OpTest, FloorMod) {
UsePositiveInput();
TestBinaryOp(kTfLiteBuiltinFloorMod, kTfLiteFloat32);
}
TEST_F(OpTest, Maximum) {
TestBinaryOp(kTfLiteBuiltinMaximum, kTfLiteFloat32);
}
TEST_F(OpTest, Minimum) {
TestBinaryOp(kTfLiteBuiltinMinimum, kTfLiteFloat32);
}
TEST_F(OpTest, Mul) {
TestBinaryOp(kTfLiteBuiltinMul, kTfLiteFloat32);
}
TEST_F(OpTest, Pow) {
// The default positive range is too big for power operation.
SetRandomInputRange(1, 5);
TestBinaryOp(kTfLiteBuiltinPow, kTfLiteFloat32);
}
TEST_F(OpTest, SquaredDifference) {
TestBinaryOp(kTfLiteBuiltinSquaredDifference, kTfLiteFloat32);
}
TEST_F(OpTest, Sub) {
TestBinaryOp<TfLiteSubParams>(kTfLiteBuiltinSub, kTfLiteFloat32);
}
TEST_F(OpTest, Conv2d) {
SingleOpModelBuilder mb;
// The input tensors are input, filter and an optional bias.
mb.AddInput(kTfLiteFloat32, {2, 5, 4, 3});
mb.AddInput(kTfLiteFloat32, {3, 3, 2, 3});
mb.AddConstInput<float>(kTfLiteFloat32, {3}, {1, 2, 3});
mb.AddOutput(kTfLiteFloat32, {2, 5, 4, 3});
mb.AddOperator<TfLiteConvParams>(kTfLiteBuiltinConv2d,
{
.padding = kTfLitePaddingSame,
.stride_width = 1,
.stride_height = 1,
.activation = kTfLiteActNone,
.dilation_width_factor = 1,
.dilation_height_factor = 1,
});
TestModel(mb);
}
TEST_F(OpTest, DepthwiseConv2d) {
SingleOpModelBuilder mb;
// The input tensors are input, filter and an optional bias.
mb.AddInput(kTfLiteFloat32, {2, 5, 4, 3});
mb.AddInput(kTfLiteFloat32, {1, 3, 2, 3});
mb.AddConstInput<float>(kTfLiteFloat32, {3}, {1, 2, 3});
mb.AddOutput(kTfLiteFloat32, {2, 5, 4, 3});
mb.AddOperator<TfLiteDepthwiseConvParams>(kTfLiteBuiltinDepthwiseConv2d,
{
.padding = kTfLitePaddingSame,
.stride_width = 1,
.stride_height = 1,
.depth_multiplier = 1,
.activation = kTfLiteActNone,
.dilation_width_factor = 1,
.dilation_height_factor = 1,
});
TestModel(mb);
}
TEST_F(OpTest, Concatenation) {
std::vector<int> shape_in = {1, 2, 3, 4};
// Concatenation allows negative indexing on the `axis` attribute.
for (int axis : {1, -1}) {
std::vector<int> shape_out = shape_in;
shape_out[(axis + 4) % 4] *= 2;
SingleOpModelBuilder mb;
mb.AddInput(kTfLiteFloat32, shape_in);
mb.AddInput(kTfLiteFloat32, shape_in);
mb.AddOutput(kTfLiteFloat32, shape_out);
mb.AddOperator<TfLiteConcatenationParams>(kTfLiteBuiltinConcatenation,
{
.axis = axis,
.activation = kTfLiteActNone,
});
}
}
} // namespace tflite::cros::tests
int main(int argc, char** argv) {
g_env = new StableDelegateEnvironment();
auto config_flags = ::tflite::cros::tests::g_config.GetFlags();
if (!g_env->InitFromCommandLine(&argc, argv, config_flags)) {
delete g_env;
return EXIT_FAILURE;
}
testing::InitGoogleTest(&argc, argv);
// GoogleTest takes the ownership of g_env.
::testing::AddGlobalTestEnvironment(g_env);
return RUN_ALL_TESTS();
}