[mxnet] Preliminary version of tensor_filter_mxnet
authorBumsik Kim <k.bumsik@gmail.com>
Wed, 14 Oct 2020 02:36:56 +0000 (19:36 -0700)
committerMyungJoo Ham <myungjoo.ham@samsung.com>
Thu, 26 May 2022 13:41:54 +0000 (09:41 -0400)
This patch newely adds MXNet support for tensor_filter.
This is based on tensor_filter_subplugin class. This currently does not
suppor the GPU acceleration but it will be added in later patches.

Since this project does not have a stable C++ coding convention, I
partially followed the Google C++ Style Guide [1] for naming and applied
.clang_format file in the root.

[1]: https://google.github.io/styleguide/cppguide.html#Naming

Signed-off-by: Bumsik Kim <k.bumsik@gmail.com>
ext/nnstreamer/tensor_filter/tensor_filter_mxnet.cc [new file with mode: 0644]

diff --git a/ext/nnstreamer/tensor_filter/tensor_filter_mxnet.cc b/ext/nnstreamer/tensor_filter/tensor_filter_mxnet.cc
new file mode 100644 (file)
index 0000000..5d2a80b
--- /dev/null
@@ -0,0 +1,480 @@
+/* SPDX-License-Identifier: LGPL-2.1-only */
+/**
+ * GStreamer Tensor_Filter, MXNet Module
+ * Copyright (C) 2020 Bumsik Kim <k.bumsik@gmail.com>
+ */
+/**
+ * @file       tensor_filter_mxnet.cc
+ * @date       5 Oct 2020
+ * @brief      MXNet module for tensor_filter gstreamer plugin
+ * @see                http://github.com/nnstreamer/nnstreamer
+ * @author     Bumsik Kim <k.bumsik@gmail.com>
+ * @bug                No known bugs except for NYI items
+ *
+ * This is the MXNet plugin for tensor_filter.
+ *
+ * @details Usage examples
+ *
+ *          Case 1: simple ImageNet example found in test case:
+ *                  https://github.com/nnstreamer/nnstreamer/tree/main/tests/nnstreamer_filter_mxnet/simple_test_mxnet.cc
+ *            gst-launch-1.0 \
+ *               appsrc ! application/octet-stream \
+ *               ! tensor_converter input-dim=1:3:224:224 input-type=float32 \
+ *               ! tensor_filter \
+ *                   framework=mxnet \
+ *                   model=model/Inception-BN.json \
+ *                   input=1:3:224:224 \
+ *                   inputtype=float32 \
+ *                   inputname=data \
+ *                   output=1 \
+ *                   outputtype=float32 \
+ *                   outputname=argmax_channel \
+ *                   custom=input_rank=4 \
+ *                   accelerator=true:cpu,!npu,!gpu \
+ *               ! appsink",
+ *
+ * @note Special considerations on props:
+ *
+ *   outputname:
+ *     The output name should not be the name of output layer, but the name of
+ *     MXNet NDArray operator passed to the output. For example, the most common
+ *     name is "argmax_channel" for typical classification model. It also can be
+ *     "sigmoid" or etc.
+ *     If it is not meant to do any operations, "_copyto" operator may be used.
+ *     The output will be passed as-is.
+ *     The current limitation is that it only takes a unary operator, meaning it
+ *     does not take additional parameters.
+ *     The candidates of operators can be found here:
+ *      - https://github.com/apache/incubator-mxnet/blob/1.7.0/benchmark/opperf/nd_operations/unary_operators.py#L24-L28
+ *      - https://mxnet.apache.org/versions/1.7/api/python/docs/api/ndarray/op/index.html?highlight=argmax_channel#mxnet.ndarray.op.argmax_channel
+ *
+ *     If this prop is not configured appropriately, the application may get
+ *     unexpected output values, or even segfaults without warning messages.
+ *
+ *   custom:
+ *     Each entries are separated by ','
+ *     Each entries have property_key=value format.
+ *     There must be no speces.
+ *
+ *     Supported props:
+ *       input_rank: (mandatory)
+ *            Rank of each input tensors.
+ *            Each ranks are separeted by ':'.
+ *            The number of ranks must be the same as the number of input
+ *            tensors.
+ *
+ *     Examples:
+ *       tensor_filter framework=mxnet model=model/Inception-BN.json
+ *                input=1:3:224:224
+ *                inputname=data
+ *                ...
+ *                custom=input_rank=4
+ *
+ *        tensor_filter framework=mxnet model=model/Inception-BN.json
+ *                input=1:3:224:224,1
+ *                inputname=data1,data2
+ *                ...
+ *                custom=input_rank=4:1
+ */
+#include <nnstreamer_cppplugin_api_filter.hh>
+#include <stdexcept>
+#include <tensor_common.h>
+
+#include <fcntl.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <mxnet-cpp/MxNetCpp.h>
+
+using nnstreamer::tensor_filter_subplugin;
+using namespace mxnet::cpp;
+
+namespace nnstreamer
+{
+namespace tensorfilter_mxnet
+{
+
+const static std::string kFileLocation = "/ext/nnstreamer/tensor_filter/tensor_filter_mxnet.cc";
+const static std::string kFileUrl = "https://github.com/nnstreamer/nnstreamer/tree/main" + kFileLocation;
+
+void init_filter_mxnet (void) __attribute__ ((constructor)); /**< Dynamic library contstructor */
+void fini_filter_mxnet (void) __attribute__ ((destructor)); /**< Dynamic library desctructor */
+
+class TensorFilterMXNet final : public tensor_filter_subplugin
+{
+  public:
+  static void init_filter (); /**< Dynamic library contstructor helper */
+  static void fini_filter (); /**< Dynamic library desctructor helper */
+
+  TensorFilterMXNet ();
+  ~TensorFilterMXNet ();
+
+  /**< Implementations of tensor_filter_subplugin */
+  tensor_filter_subplugin &getEmptyInstance ();
+  void configure_instance (const GstTensorFilterProperties *prop);
+  void invoke (const GstTensorMemory *input, GstTensorMemory *output);
+  void getFrameworkInfo (GstTensorFilterFrameworkInfo &info);
+  int getModelInfo (model_info_ops ops, GstTensorsInfo &in_info, GstTensorsInfo &out_info);
+  int eventHandler (event_ops ops, GstTensorFilterFrameworkEventData &data);
+
+  static const std::string ext_symbol; /**< extension of model symbol (.json) */
+  static const std::string ext_params; /**< extension of model parameters (.params) */
+
+  /**< Data type for MXNet NDArray from <mshadow/base.h> which causes errors when included */
+  enum TypeFlag : int {
+    kFloat32 = 0,
+    kFloat64 = 1,
+    kFloat16 = 2,
+    kUint8 = 3,
+    kInt32 = 4,
+    kInt8 = 5,
+    kInt64 = 6,
+    kBool = 7,
+    kInt16 = 8,
+    kUint16 = 9,
+    kUint32 = 10,
+    kUint64 = 11,
+    kBfloat16 = 12
+  };
+
+  private:
+  Shape tensorInfoToShape (GstTensorInfo &tensorinfo, int rank);
+  TypeFlag tensorTypeToMXNet (tensor_type type);
+  void parseCustomProperties (const GstTensorFilterProperties *prop);
+
+  bool empty_model_; /**< Empty (not initialized) model flag */
+  static const GstTensorFilterFrameworkInfo info_; /**< Framework info */
+  GstTensorsInfo inputs_info_; /**< Input tensors metadata */
+  GstTensorsInfo outputs_info_; /**< Output tensors metadata */
+
+  // GstTensorInfo does not contain rank info, so extra fields needed */
+  int input_ranks_[NNS_TENSOR_RANK_LIMIT]; /**< Rank info of input tensor */
+  int output_ranks_[NNS_TENSOR_RANK_LIMIT]; /**< Rank info of output tensor */
+
+  std::string model_symbol_path_; /**< The model symbol .json file */
+  std::string model_params_path_; /**< The model paremeters .params file */
+
+  Symbol net_; /**< Model symbol */
+  std::unique_ptr<Executor> executor_; /**< Model executor */
+  std::map<std::string, NDArray> args_map_; /**< arguments information of model, used internally by MXNet */
+  std::map<std::string, NDArray> aux_map_; /**< auxiliary information of model, used internally by MXNet */
+  Context ctx_; /**< Device type (CPU or GPU) */
+
+  static TensorFilterMXNet *registeredRepresentation;
+};
+
+const std::string TensorFilterMXNet::ext_symbol = ".json";
+const std::string TensorFilterMXNet::ext_params = ".params";
+
+const GstTensorFilterFrameworkInfo TensorFilterMXNet::info_ = { .name = "mxnet",
+  .allow_in_place = FALSE,
+  .allocate_in_invoke = FALSE,
+  .run_without_model = FALSE,
+  .verify_model_path = TRUE,
+  .hw_list = (const accl_hw[]){ ACCL_CPU },
+  .num_hw = 1,
+  .accl_auto = static_cast<accl_hw>(-1),
+  .accl_default = static_cast<accl_hw>(-1),
+  .statistics = nullptr };
+
+TensorFilterMXNet::TensorFilterMXNet ()
+    : tensor_filter_subplugin (), empty_model_ (true), ctx_ (Context::cpu ())
+{
+  /** Nothing to do. Just let it have an empty instance */
+}
+
+TensorFilterMXNet::~TensorFilterMXNet ()
+{
+  executor_.reset ();
+}
+
+tensor_filter_subplugin &
+TensorFilterMXNet::getEmptyInstance ()
+{
+  return *(new TensorFilterMXNet ());
+}
+
+void
+TensorFilterMXNet::configure_instance (const GstTensorFilterProperties *prop)
+{
+  if (prop->num_models != 1) {
+    throw std::invalid_argument ("Multiple models is not supported.");
+  }
+
+  try {
+    parseCustomProperties (prop);
+  } catch (const std::invalid_argument &e) {
+    throw std::invalid_argument ("Failed to parse \"custom\" prop:"
+                                 + std::string (e.what ()) + "\n\tReference: " + kFileUrl);
+  }
+
+  // Validate model file paths and then assign
+  std::tie (model_symbol_path_, model_params_path_) = [&] {
+    const std::vector<std::string> extensions{ TensorFilterMXNet::ext_symbol,
+      TensorFilterMXNet::ext_params };
+
+    // trim extension like .json
+    const std::string model_path = [&] {
+      std::string path (prop->model_files[0]);
+      for (const std::string &ext : extensions) {
+        if (g_str_has_suffix (path.c_str (), ext.c_str ())) {
+          return path.substr (0, path.length () - ext.length ());
+        }
+      }
+      return path;
+    }();
+
+    // validate paths
+    for (const std::string &ext : extensions) {
+      std::string path (model_path + ext);
+      if (g_access (path.c_str (), R_OK) != 0) {
+        throw std::invalid_argument ("Failed to open the " + ext + " file, " + path);
+      }
+    }
+
+    return std::make_tuple (model_path + TensorFilterMXNet::ext_symbol,
+        model_path + TensorFilterMXNet::ext_params);
+  }();
+
+  // Read a model
+  net_ = Symbol::Load (model_symbol_path_);
+
+  // Load parameters into temporary array maps
+  // The following loop split loaded param map into arg parm
+  // and aux param with target context
+  std::map<std::string, NDArray> parameters;
+  NDArray::Load (model_params_path_, nullptr, &parameters);
+  for (const auto &pair : parameters) {
+    std::string type = pair.first.substr (0, 4);
+    std::string name = pair.first.substr (4);
+    if (type == "arg:") {
+      args_map_[name] = pair.second.Copy (ctx_);
+    } else if (type == "aux:") {
+      aux_map_[name] = pair.second.Copy (ctx_);
+    }
+  }
+
+  // WaitAll is need when we copy data between GPU and the main memory
+  NDArray::WaitAll ();
+
+  gst_tensors_info_copy (&inputs_info_, &prop->input_meta);
+  gst_tensors_info_copy (&outputs_info_, &prop->output_meta);
+
+  // Set ndarrays for the input layers
+  for (unsigned int i = 0; i < inputs_info_.num_tensors; i++) {
+    auto &input_tensor = inputs_info_.info[i];
+    args_map_[input_tensor.name]
+        = NDArray (tensorInfoToShape (input_tensor, input_ranks_[i]), ctx_,
+            false, tensorTypeToMXNet (input_tensor.type));
+  }
+
+  // These are ndarrays where the execution engine runs
+  std::vector<NDArray> arg_arrays;
+  std::vector<NDArray> grad_arrays;
+  std::vector<OpReqType> grad_reqs;
+  std::vector<NDArray> aux_arrays;
+
+  try {
+    // infer and create ndarrays according to the given array maps.
+    net_.InferExecutorArrays (ctx_, &arg_arrays, &grad_arrays, &grad_reqs,
+        &aux_arrays, args_map_, std::map<std::string, NDArray> (),
+        std::map<std::string, OpReqType> (), aux_map_);
+    for (auto& i : grad_reqs) i = OpReqType::kNullOp;
+
+    // Create a symbolic execution engine after binding the model to input parameters.
+    executor_ = std::make_unique<Executor> (
+        net_, ctx_, arg_arrays, grad_arrays, grad_reqs, aux_arrays);
+  } catch (const std::exception &e) {
+    throw std::system_error (ENODEV, std::system_category (),
+        std::string (e.what ()) + "\nFailed to bind parameters to the model. This is mostly caused by wrong \"input\" and \"inputname\" information.");
+  }
+  empty_model_ = false;
+}
+
+void
+TensorFilterMXNet::invoke (const GstTensorMemory *input, GstTensorMemory *output)
+{
+  assert (!empty_model_);
+  assert (executor_);
+
+  // Copy input
+  for (unsigned int i = 0; i < inputs_info_.num_tensors; i++) {
+    auto &input_info = inputs_info_.info[i];
+    auto &input_ndarray = args_map_[input_info.name];
+
+    assert ((input_ndarray.Size () * sizeof (mx_float)) == input[i].size);
+    input_ndarray.SyncCopyFromCPU (
+        (const mx_float *)input[i].data, input_ndarray.Size ());
+    NDArray::WaitAll ();
+  }
+
+  // Run forward pass
+  executor_->Forward (false);
+  NDArray::WaitAll ();
+
+  // Copy output
+  for (unsigned int i = 0; i < outputs_info_.num_tensors; i++) {
+    auto &output_info = outputs_info_.info[i];
+    NDArray result;
+
+    // Warning: It will cause segfault if the operator name (output name) is different from expected.
+    //          The user should know the name of the operator name.
+    Operator (output_info.name) (executor_->outputs[0]).Invoke (result);
+    NDArray::WaitAll ();
+
+    assert ((result.Size () * sizeof (mx_float)) == output[i].size);
+    result.SyncCopyToCPU ((mx_float *)output[i].data, result.Size ());
+    NDArray::WaitAll ();
+  }
+}
+
+void
+TensorFilterMXNet::getFrameworkInfo (GstTensorFilterFrameworkInfo &info)
+{
+  info = info_;
+}
+
+int
+TensorFilterMXNet::getModelInfo (
+    model_info_ops ops, GstTensorsInfo &in_info, GstTensorsInfo &out_info)
+{
+  switch (ops) {
+  case GET_IN_OUT_INFO:
+    gst_tensors_info_copy (std::addressof (in_info), std::addressof (inputs_info_));
+    gst_tensors_info_copy (std::addressof (out_info), std::addressof (outputs_info_));
+    break;
+  case SET_INPUT_INFO:
+  default:
+    return -ENOENT;
+  }
+  return 0;
+}
+
+int
+TensorFilterMXNet::eventHandler (event_ops ops, GstTensorFilterFrameworkEventData &data)
+{
+  return -ENOENT;
+}
+
+/**
+ * @brief Convert GstTensorInfo to MXNet Shape
+ */
+Shape
+TensorFilterMXNet::tensorInfoToShape (GstTensorInfo &tensorinfo, int rank)
+{
+  return Shape (std::vector<index_t> (tensorinfo.dimension, tensorinfo.dimension + rank));
+}
+
+/**
+ * @brief Convert tensor_type to MXNet TypeFlag
+ */
+TensorFilterMXNet::TypeFlag
+TensorFilterMXNet::tensorTypeToMXNet (tensor_type type)
+{
+  switch (type) {
+  case _NNS_INT32:
+    return kInt32;
+  case _NNS_UINT32:
+    return kUint32;
+  case _NNS_INT16:
+    return kInt16;
+  case _NNS_UINT16:
+    return kUint16;
+  case _NNS_INT8:
+    return kInt8;
+  case _NNS_UINT8:
+    return kUint8;
+  case _NNS_FLOAT64:
+    return kFloat64;
+  case _NNS_FLOAT32:
+    return kFloat32;
+  case _NNS_INT64:
+    return kInt64;
+  case _NNS_UINT64:
+    return kUint64;
+  default:
+    throw std::invalid_argument ("Unsupported data type.");
+  }
+}
+
+/**
+ * @brief Parse custom prop and set instance options accordingly.
+ */
+void
+TensorFilterMXNet::parseCustomProperties (const GstTensorFilterProperties *prop)
+{
+  using unique_g_strv = std::unique_ptr<gchar *, std::function<void (gchar **)>>;
+  bool is_input_rank_parsed = false;
+
+  if (prop->custom_properties) {
+    unique_g_strv options (g_strsplit (prop->custom_properties, ",", -1), g_strfreev);
+    guint len = g_strv_length (options.get ());
+
+    for (guint i = 0; i < len; i++) {
+      unique_g_strv option (g_strsplit (options.get ()[i], "=", -1), g_strfreev);
+
+      if (g_strv_length (option.get ()) > 1) {
+        g_strstrip (option.get ()[0]);
+        g_strstrip (option.get ()[1]);
+
+        if (g_ascii_strcasecmp (option.get ()[0], "input_rank") == 0) {
+          unique_g_strv ranks (g_strsplit (option.get ()[1], ":", -1), g_strfreev);
+          guint num_outputs = g_strv_length (ranks.get ());
+
+          if (num_outputs != prop->input_meta.num_tensors) {
+            throw std::invalid_argument (
+                "The number of ranks does not match the number of input tensors.");
+          }
+
+          for (guint i = 0; i < num_outputs; i++) {
+            input_ranks_[i] = g_ascii_strtoull (ranks.get ()[i], nullptr, 10);
+          }
+          is_input_rank_parsed = true;
+        } else {
+          throw std::invalid_argument (
+              "Unsupported custom property: " + std::string (option.get ()[0]) + ".");
+        }
+      }
+    }
+  }
+
+  if (!is_input_rank_parsed) {
+    throw std::invalid_argument ("\"input_rank\" must be set. e.g. custom=input_rank=4:1");
+  }
+  return;
+}
+
+TensorFilterMXNet *TensorFilterMXNet::registeredRepresentation = nullptr;
+
+/** @brief Initialize this object for tensor_filter subplugin runtime register */
+void
+TensorFilterMXNet::init_filter (void)
+{
+  registeredRepresentation
+      = tensor_filter_subplugin::register_subplugin<TensorFilterMXNet> ();
+}
+
+void
+init_filter_mxnet ()
+{
+  TensorFilterMXNet::init_filter ();
+}
+
+/** @brief Destruct the subplugin */
+void
+TensorFilterMXNet::fini_filter (void)
+{
+  assert (registeredRepresentation != nullptr);
+  tensor_filter_subplugin::unregister_subplugin (registeredRepresentation);
+}
+
+void
+fini_filter_mxnet ()
+{
+  TensorFilterMXNet::fini_filter ();
+}
+
+} // namespace tensorfilter_mxnet
+} /* namespace nnstreamer */