Introduce NNAPI Unittest through T/F Lite (#374)
author박종현/동작제어Lab(SR)/Senior Engineer/삼성전자 <jh1302.park@samsung.com>
Mon, 2 Apr 2018 23:11:58 +0000 (08:11 +0900)
committer김정현/동작제어Lab(SR)/Senior Engineer/삼성전자 <jh0822.kim@samsung.com>
Mon, 2 Apr 2018 23:11:58 +0000 (08:11 +0900)
This commit introduces a convolution NNAPI unittest which
invokes NN API via T/F Lite interpreter and compare it with
the output of T/F Lite pure interpreter.

This testcase is derived from a convolution example in tflite_examples.

Signed-off-by: Jonghyun Park <jh1302.park@samsung.com>
tools/CMakeLists.txt
tools/nnapi_unittests/CMakeLists.txt [new file with mode: 0644]
tools/nnapi_unittests/inc/env.h [new file with mode: 0644]
tools/nnapi_unittests/inc/nn.h [new file with mode: 0644]
tools/nnapi_unittests/lib/env.cpp [new file with mode: 0644]
tools/nnapi_unittests/lib/nn.cpp [new file with mode: 0644]
tools/nnapi_unittests/tests/conv_1.cpp [new file with mode: 0644]
tools/nnapi_unittests/tests/conv_1.lst [new file with mode: 0644]

index 033b73b..cf71ba7 100644 (file)
@@ -7,3 +7,4 @@ add_subdirectory(tflite_benchmark)
 add_subdirectory(tflite_examples)
 add_subdirectory(nnapi_bindings)
 add_subdirectory(nnapi_test)
+add_subdirectory(nnapi_unittests)
diff --git a/tools/nnapi_unittests/CMakeLists.txt b/tools/nnapi_unittests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..61c50ee
--- /dev/null
@@ -0,0 +1,8 @@
+file(GLOB_RECURSE NNAPI_UNITTEST_LIB_SOURCES "lib/*.cpp")
+
+add_library(nnapi_unitest_common "${NNAPI_UNITTEST_LIB_SOURCES}")
+target_include_directories(nnapi_unitest_common PUBLIC "inc")
+target_link_libraries(nnapi_unitest_common tensorflow_lite)
+
+add_executable(nnapi_unittest_conv_1 "tests/conv_1.cpp")
+target_link_libraries(nnapi_unittest_conv_1 nnapi_unitest_common)
diff --git a/tools/nnapi_unittests/inc/env.h b/tools/nnapi_unittests/inc/env.h
new file mode 100644 (file)
index 0000000..05c59e4
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef __ENV_UTILS_H__
+#define __ENV_UTILS_H__
+
+#include <string>
+
+#include <cstdint>
+
+class IntVar
+{
+public:
+  IntVar(const std::string &name, int32_t value);
+
+public:
+  int32_t operator()(void) const { return _value; }
+
+private:
+  int32_t _value;
+};
+
+#endif // __ENV_UTILS_H__
diff --git a/tools/nnapi_unittests/inc/nn.h b/tools/nnapi_unittests/inc/nn.h
new file mode 100644 (file)
index 0000000..ac96a11
--- /dev/null
@@ -0,0 +1,124 @@
+#ifndef __NN_H__
+#define __NN_H__
+
+#include <vector>
+
+#include <cstdint>
+
+namespace vector
+{
+
+template <typename T> struct View
+{
+  virtual ~View() = default;
+
+  virtual int32_t size(void) const = 0;
+  virtual T at(uint32_t off) const = 0;
+};
+
+}
+
+namespace feature
+{
+
+struct Shape
+{
+  int32_t C;
+  int32_t H;
+  int32_t W;
+
+  Shape() = default;
+  Shape(int32_t depth, int32_t height, int32_t width)
+      : C{depth}, H{height}, W{width}
+  {
+    // DO NOTHING
+  }
+};
+
+template <typename T> struct View
+{
+  virtual ~View() = default;
+
+  virtual const Shape &shape(void) const = 0;
+  virtual T at(uint32_t ch, uint32_t row, uint32_t col) const = 0;
+};
+
+}
+
+namespace kernel
+{
+
+struct Shape
+{
+  int32_t N;
+  int32_t C;
+  int32_t H;
+  int32_t W;
+
+  Shape() = default;
+  Shape(int32_t count, int32_t depth, int32_t height, int32_t width)
+      : N{count}, C{depth}, H{height}, W{width}
+  {
+    // DO NOTHING
+  }
+};
+
+template <typename T> struct View
+{
+  virtual ~View() = default;
+
+  virtual const Shape &shape(void) const = 0;
+  virtual T at(uint32_t nth, uint32_t ch, uint32_t row, uint32_t col) const = 0;
+};
+
+}
+
+class SampleBiasObject final : public vector::View<float>
+{
+public:
+  SampleBiasObject(int32_t size);
+
+public:
+  int32_t size(void) const override;
+
+public:
+  float at(uint32_t off) const override;
+
+private:
+  int32_t _size;
+};
+
+class SampleFeatureObject final : public feature::View<float>
+{
+public:
+  SampleFeatureObject(const feature::Shape &shape);
+
+public:
+  const feature::Shape &shape(void) const override;
+
+  float at(uint32_t ch, uint32_t row, uint32_t col) const override;
+
+public:
+  float &at(uint32_t ch, uint32_t row, uint32_t col);
+
+private:
+  feature::Shape _shape;
+  std::vector<float> _value;
+};
+
+class SampleKernelObject final : public kernel::View<float>
+{
+public:
+  SampleKernelObject(const kernel::Shape &shape);
+
+public:
+  const kernel::Shape &shape(void) const override;
+
+  float at(uint32_t nth, uint32_t ch, uint32_t row, uint32_t col) const override;
+
+private:
+  kernel::Shape _shape;
+  std::vector<float> _value;
+};
+
+#endif
diff --git a/tools/nnapi_unittests/lib/env.cpp b/tools/nnapi_unittests/lib/env.cpp
new file mode 100644 (file)
index 0000000..c1f1e92
--- /dev/null
@@ -0,0 +1,16 @@
+#include "env.h"
+
+#include <cstdlib>
+
+//
+// Integer variable
+//
+IntVar::IntVar(const std::string &name, int32_t value) : _value{value}
+{
+  auto env = std::getenv(name.c_str());
+
+  if (env != nullptr)
+  {
+    _value = std::atoi(env);
+  }
+}
diff --git a/tools/nnapi_unittests/lib/nn.cpp b/tools/nnapi_unittests/lib/nn.cpp
new file mode 100644 (file)
index 0000000..783496e
--- /dev/null
@@ -0,0 +1,67 @@
+#include <nn.h>
+
+#include <cassert>
+
+//
+// SampleBiasObject
+//
+SampleBiasObject::SampleBiasObject(int32_t size) : _size(size)
+{
+  // DO NOTHING
+}
+
+int32_t SampleBiasObject::size(void) const { return _size; }
+
+float SampleBiasObject::at(uint32_t off) const { return 0.0f; }
+
+//
+// SampleFeatureObject
+//
+SampleFeatureObject::SampleFeatureObject(const feature::Shape &shape) : _shape{shape}
+{
+  const uint32_t size = _shape.C * _shape.H * _shape.W;
+
+  for (uint32_t off = 0; off < size; ++off)
+  {
+    _value.emplace_back(off);
+  }
+
+  assert(_value.size() == size);
+}
+
+const feature::Shape &SampleFeatureObject::shape(void) const
+{
+  return _shape;
+}
+
+float SampleFeatureObject::at(uint32_t ch, uint32_t row, uint32_t col) const
+{
+  return _value.at(ch * _shape.H * _shape.W + row * _shape.W + col);
+}
+
+float &SampleFeatureObject::at(uint32_t ch, uint32_t row, uint32_t col)
+{
+  return _value.at(ch * _shape.H * _shape.W + row * _shape.W + col);
+}
+
+//
+// SampleKernelObject
+//
+SampleKernelObject::SampleKernelObject(const kernel::Shape &shape) : _shape{shape}
+{
+  const uint32_t size = _shape.N * _shape.C * _shape.H * _shape.W;
+
+  for (uint32_t off = 0; off < size; ++off)
+  {
+    _value.emplace_back(static_cast<float>(off));
+  }
+
+  assert(_value.size() == size);
+}
+
+const kernel::Shape &SampleKernelObject::shape(void) const { return _shape; };
+
+float SampleKernelObject::at(uint32_t nth, uint32_t ch, uint32_t row, uint32_t col) const
+{
+  return _value.at(nth * _shape.C * _shape.H * _shape.W + ch * _shape.H * _shape.W + row * _shape.W + col);
+}
diff --git a/tools/nnapi_unittests/tests/conv_1.cpp b/tools/nnapi_unittests/tests/conv_1.cpp
new file mode 100644 (file)
index 0000000..c7e598a
--- /dev/null
@@ -0,0 +1,215 @@
+#include "tensorflow/contrib/lite/kernels/register.h"
+#include "tensorflow/contrib/lite/model.h"
+#include "tensorflow/contrib/lite/builtin_op_data.h"
+
+#include "nn.h"
+#include "env.h"
+
+#include <iostream>
+
+using namespace tflite;
+using namespace tflite::ops::builtin;
+
+int main(int argc, char **argv)
+{
+#define INT_VALUE(NAME, VALUE) IntVar NAME##_Value(#NAME, VALUE);
+#include "conv_1.lst"
+#undef INT_VALUE
+
+  const int32_t IFM_C = IFM_C_Value();
+  const int32_t IFM_H = IFM_H_Value();
+  const int32_t IFM_W = IFM_W_Value();
+
+  const int32_t KER_N = KER_N_Value();
+  const int32_t KER_C = IFM_C_Value();
+  const int32_t KER_H = KER_H_Value();
+  const int32_t KER_W = KER_W_Value();
+
+  const int32_t OFM_C = KER_N;
+  const int32_t OFM_H = (IFM_H - KER_H) + 1;
+  const int32_t OFM_W = (IFM_W - KER_W) + 1;
+
+  const SampleFeatureObject ifm{feature::Shape{IFM_C, IFM_H, IFM_W}};
+  const SampleKernelObject kernel{kernel::Shape{KER_N, KER_C, KER_H, KER_W}};
+  const SampleBiasObject bias{KER_N};
+
+  // Configure Kernel Data
+  const uint32_t kernel_size = KER_N * KER_C * KER_H * KER_W;
+  float kernel_data[kernel_size] = { 0.0f, };
+
+  // Fill kernel data in NHWC order
+  {
+    uint32_t off = 0;
+
+    for (uint32_t nth = 0; nth < KER_N; ++nth)
+    {
+      for (uint32_t row = 0; row < KER_H; ++row)
+      {
+        for (uint32_t col = 0; col < KER_W; ++col)
+        {
+          for (uint32_t ch = 0; ch < KER_C; ++ch)
+          {
+            const auto value = kernel.at(nth, ch, row, col);
+            kernel_data[off++] = value;
+          }
+        }
+      }
+    }
+
+    assert(kernel_size == off);
+  }
+
+  // Assumption on this example
+  assert(IFM_C == KER_C);
+  assert(KER_N == bias.size());
+
+  auto setup = [&](Interpreter &interp)
+  {
+    // Comment from 'context.h'
+    //
+    // Parameters for asymmetric quantization. Quantized values can be converted
+    // back to float using:
+    //    real_value = scale * (quantized_value - zero_point);
+    //
+    // Q: Is this necessary?
+    TfLiteQuantizationParams quantization;
+
+    quantization.scale = 1;
+    quantization.zero_point = 0;
+
+    // On AddTensors(N) call, T/F Lite interpreter creates N tensors whose index is [0 ~ N)
+    interp.AddTensors(5);
+
+    // Configure OFM
+    interp.SetTensorParametersReadWrite(0,
+                                        kTfLiteFloat32 /* type */,
+                                        "output" /* name */,
+                                        {1 /*N*/, OFM_H, OFM_W, OFM_C} /* dims */,
+                                        quantization);
+
+
+    // Configure IFM
+    interp.SetTensorParametersReadWrite(1,
+                                        kTfLiteFloat32 /* type */,
+                                        "input" /* name */,
+                                        {1 /*N*/, IFM_H, IFM_W, IFM_C} /* dims */,
+                                        quantization);
+
+    // NOTE kernel_data should live longer than interpreter!
+    interp.SetTensorParametersReadOnly(2,
+                                       kTfLiteFloat32 /* type */,
+                                       "filter" /* name */,
+                                       {KER_N, KER_H, KER_W, KER_C} /* dims */,
+                                       quantization,
+                                       reinterpret_cast<const char *>(kernel_data), kernel_size * sizeof(float));
+
+    // Configure Bias
+    const uint32_t bias_size = bias.size();
+    float bias_data[bias_size] = { 0.0f, };
+
+    // Fill bias data
+    for (uint32_t off = 0; off < bias.size(); ++off)
+    {
+      bias_data[off] = bias.at(off);
+    }
+
+    interp.SetTensorParametersReadOnly(3,
+                                       kTfLiteFloat32 /* type */,
+                                       "bias" /* name */,
+                                       { bias.size() } /* dims */,
+                                       quantization,
+                                       reinterpret_cast<const char *>(bias_data), sizeof(bias_data));
+
+    // Add Convolution Node
+    //
+    // NOTE AddNodeWithParameters take the ownership of param, and deallocate it with free
+    //      So, param should be allocated with malloc
+    TfLiteConvParams *param = reinterpret_cast<TfLiteConvParams *>(malloc(sizeof(TfLiteConvParams)));
+
+    param->padding = kTfLitePaddingValid;
+    param->stride_width = 1;
+    param->stride_height = 1;
+    param->activation = kTfLiteActRelu;
+
+    // Run Convolution and store its result into Tensor #0
+    //  - Read IFM from Tensor #1
+    //  - Read Filter from Tensor #2,
+    //  - Read Bias from Tensor #3
+    interp.AddNodeWithParameters({1, 2, 3}, {0},
+                                 nullptr, 0,
+                                 reinterpret_cast<void *>(param),
+                                 BuiltinOpResolver().FindOp(BuiltinOperator_CONV_2D));
+
+    // Set Tensor #1 as Input #0, and Tensor #0 as Output #0
+    interp.SetInputs({1});
+    interp.SetOutputs({0});
+
+    // Allocate Tensor
+    interp.AllocateTensors();
+
+    // Fill IFM data in HWC order
+    {
+      uint32_t off = 0;
+
+      for (uint32_t row = 0; row < ifm.shape().H; ++row)
+      {
+        for (uint32_t col = 0; col < ifm.shape().W; ++col)
+        {
+          for (uint32_t ch = 0; ch < ifm.shape().C; ++ch)
+          {
+            const auto value = ifm.at(ch, row, col);
+            interp.typed_input_tensor<float>(0)[off++] = value;
+          }
+        }
+      }
+    }
+  };
+
+  Interpreter pure;
+  {
+    pure.UseNNAPI(false);
+
+    setup(pure);
+
+    // Let's Rock-n-Roll!
+    pure.Invoke();
+  }
+
+  Interpreter delegated;
+  {
+    // Let's use NNAPI (if possible)
+    delegated.UseNNAPI(true);
+
+    setup(delegated);
+
+    delegated.Invoke();
+  }
+
+  // Compare OFM
+  {
+    uint32_t off = 0;
+
+    for (uint32_t row = 0; row < OFM_H; ++row)
+    {
+      for (uint32_t col = 0; col < OFM_W; ++col)
+      {
+        for (uint32_t ch = 0; ch < kernel.shape().N; ++ch)
+        {
+          const auto value_from_interp = pure.typed_output_tensor<float>(0)[off];
+          const auto value_from_NNAPI = delegated.typed_output_tensor<float>(0)[off];
+
+          if (value_from_interp != value_from_NNAPI)
+          {
+            std::cerr << "Diff at (ch: " << ch << ", row: " << row << ", col: " << col << ")" << std::endl;
+            std::cerr << "  Value from interpreter: " << value_from_interp << std::endl;
+            std::cerr << "  Value from NNAPI: " << value_from_NNAPI << std::endl;
+          }
+
+          off += 1;
+        }
+      }
+    }
+  }
+
+  return 0;
+}
diff --git a/tools/nnapi_unittests/tests/conv_1.lst b/tools/nnapi_unittests/tests/conv_1.lst
new file mode 100644 (file)
index 0000000..45dc065
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef INT_VALUE
+#error "INT_VALUE should be defined"
+#endif // INT_VALUE
+
+INT_VALUE(IFM_C, 2)
+INT_VALUE(IFM_H, 3)
+INT_VALUE(IFM_W, 4)
+
+INT_VALUE(KER_N, 1)
+INT_VALUE(KER_H, 3)
+INT_VALUE(KER_W, 4)