Реализация пользовательского делегата

Что такое делегат TensorFlow Lite?

Делегат TensorFlow Lite позволяет запускать ваши модели (частично или полностью) на другом исполнителе. Этот механизм может использовать различные ускорители на устройстве, такие как GPU или Edge TPU (Tensor Processing Unit) для логического вывода. Это предоставляет разработчикам гибкий и отделенный от стандартного TFLite метод для ускорения логического вывода.

На приведенной ниже диаграмме представлены делегаты, более подробная информация приведена в следующих разделах.

TFLite Delegates

Когда мне следует создать настраиваемого делегата?

TensorFlow Lite имеет множество делегатов для целевых ускорителей, таких как GPU, DSP, EdgeTPU и фреймворков, таких как Android NNAPI.

Создание собственного делегата полезно в следующих сценариях:

  • Вы хотите интегрировать новый механизм вывода ML, который не поддерживается ни одним существующим делегатом.
  • У вас есть собственный аппаратный ускоритель, который улучшает время выполнения для известных сценариев.
  • Вы разрабатываете оптимизации ЦП (например, слияние операторов), которые могут ускорить определенные модели.

Как работают делегаты?

Рассмотрим простой граф модели, подобный приведенному ниже, и делегат MyDelegate с более быстрой реализацией операций Conv2D и Mean.

Original graph

После применения этого «MyDelegate» исходный график TensorFlow Lite будет обновлен следующим образом:

Graph with delegate

График выше получен, поскольку TensorFlow Lite разбивает исходный график по двум правилам:

  • Конкретные операции, которые могут быть обработаны делегатом, помещаются в раздел, при этом все еще удовлетворяя исходным зависимостям рабочего процесса вычислений между операциями.
  • Каждый делегируемый раздел имеет только входные и выходные узлы, которые не обрабатываются делегатом.

Каждый раздел, который обрабатывается делегатом, заменяется узлом делегата (также может называться ядром делегата) в исходном графе, который оценивает раздел при вызове вызова.

В зависимости от модели окончательный граф может иметь один или несколько узлов, последнее означает, что некоторые операции не поддерживаются делегатом. В общем, вы не хотите, чтобы делегат обрабатывал несколько разделов, потому что каждый раз, когда вы переключаетесь с делегата на основной граф, возникают накладные расходы на передачу результатов из делегированного подграфа в основной граф, который возникает из-за памяти. копий (например, с GPU на CPU). Такие накладные расходы могут компенсировать прирост производительности, особенно при наличии большого количества копий памяти.

Реализация собственного пользовательского делегата

Предпочтительный метод добавления делегата — использование SimpleDelegate API .

Чтобы создать новый делегат, вам нужно реализовать 2 интерфейса и предоставить собственную реализацию для методов интерфейса.

1 — SimpleDelegateInterface

Этот класс представляет возможности делегата, поддерживаемые операции и фабричный класс для создания ядра, инкапсулирующего делегированный граф. Дополнительные сведения см. в описании интерфейса, определенного в этом заголовочном файле C++ . Комментарии в коде подробно объясняют каждый API.

2 — SimpleDelegateKernelInterface

Этот класс инкапсулирует логику инициализации/подготовки/и запуска делегированного раздела.

Он имеет: (см. определение )

  • Init(...): который будет вызываться один раз для выполнения любой одноразовой инициализации.
  • Prepare(...): вызывается для каждого отдельного экземпляра этого узла - это происходит, если у вас есть несколько делегированных разделов. Обычно вы хотите сделать распределение памяти здесь, так как это будет вызываться каждый раз, когда тензоры изменяются.
  • Invoke(...): который будет вызываться для логического вывода.

Пример

В этом примере вы создадите очень простой делегат, который может поддерживать только 2 типа операций (ADD) и (SUB) только с тензорами float32.

// MyDelegate implements the interface of SimpleDelegateInterface.
// This holds the Delegate capabilities.
class MyDelegate : public SimpleDelegateInterface {
 public:
  bool IsNodeSupportedByDelegate(const TfLiteRegistration* registration,
                                 const TfLiteNode* node,
                                 TfLiteContext* context) const override {
    // Only supports Add and Sub ops.
    if (kTfLiteBuiltinAdd != registration->builtin_code &&
        kTfLiteBuiltinSub != registration->builtin_code)
      return false;
    // This delegate only supports float32 types.
    for (int i = 0; i < node->inputs->size; ++i) {
      auto& tensor = context->tensors[node->inputs->data[i]];
      if (tensor.type != kTfLiteFloat32) return false;
    }
    return true;
  }

  TfLiteStatus Initialize(TfLiteContext* context) override { return kTfLiteOk; }

  const char* Name() const override {
    static constexpr char kName[] = "MyDelegate";
    return kName;
  }

  std::unique_ptr<SimpleDelegateKernelInterface> CreateDelegateKernelInterface()
      override {
    return std::make_unique<MyDelegateKernel>();
  }
};

Затем создайте собственное ядро ​​делегата, наследуя от SimpleDelegateKernelInterface

// My delegate kernel.
class MyDelegateKernel : public SimpleDelegateKernelInterface {
 public:
  TfLiteStatus Init(TfLiteContext* context,
                    const TfLiteDelegateParams* params) override {
    // Save index to all nodes which are part of this delegate.
    inputs_.resize(params->nodes_to_replace->size);
    outputs_.resize(params->nodes_to_replace->size);
    builtin_code_.resize(params->nodes_to_replace->size);
    for (int i = 0; i < params->nodes_to_replace->size; ++i) {
      const int node_index = params->nodes_to_replace->data[i];
      // Get this node information.
      TfLiteNode* delegated_node = nullptr;
      TfLiteRegistration* delegated_node_registration = nullptr;
      TF_LITE_ENSURE_EQ(
          context,
          context->GetNodeAndRegistration(context, node_index, &delegated_node,
                                          &delegated_node_registration),
          kTfLiteOk);
      inputs_[i].push_back(delegated_node->inputs->data[0]);
      inputs_[i].push_back(delegated_node->inputs->data[1]);
      outputs_[i].push_back(delegated_node->outputs->data[0]);
      builtin_code_[i] = delegated_node_registration->builtin_code;
    }
    return kTfLiteOk;
  }

  TfLiteStatus Prepare(TfLiteContext* context, TfLiteNode* node) override {
    return kTfLiteOk;
  }

  TfLiteStatus Eval(TfLiteContext* context, TfLiteNode* node) override {
    // Evaluate the delegated graph.
    // Here we loop over all the delegated nodes.
    // We know that all the nodes are either ADD or SUB operations and the
    // number of nodes equals ''inputs_.size()'' and inputs[i] is a list of
    // tensor indices for inputs to node ''i'', while outputs_[i] is the list of
    // outputs for node
    // ''i''. Note, that it is intentional we have simple implementation as this
    // is for demonstration.

    for (int i = 0; i < inputs_.size(); ++i) {
      // Get the node input tensors.
      // Add/Sub operation accepts 2 inputs.
      auto& input_tensor_1 = context->tensors[inputs_[i][0]];
      auto& input_tensor_2 = context->tensors[inputs_[i][1]];
      auto& output_tensor = context->tensors[outputs_[i][0]];
      TF_LITE_ENSURE_EQ(
          context,
          ComputeResult(context, builtin_code_[i], &input_tensor_1,
                        &input_tensor_2, &output_tensor),
          kTfLiteOk);
    }
    return kTfLiteOk;
  }

 private:
  // Computes the result of addition of 'input_tensor_1' and 'input_tensor_2'
  // and store the result in 'output_tensor'.
  TfLiteStatus ComputeResult(TfLiteContext* context, int builtin_code,
                             const TfLiteTensor* input_tensor_1,
                             const TfLiteTensor* input_tensor_2,
                             TfLiteTensor* output_tensor) {
    if (NumElements(input_tensor_1) != NumElements(input_tensor_2) ||
        NumElements(input_tensor_1) != NumElements(output_tensor)) {
      return kTfLiteDelegateError;
    }
    // This code assumes no activation, and no broadcasting needed (both inputs
    // have the same size).
    auto* input_1 = GetTensorData<float>(input_tensor_1);
    auto* input_2 = GetTensorData<float>(input_tensor_2);
    auto* output = GetTensorData<float>(output_tensor);
    for (int i = 0; i < NumElements(input_tensor_1); ++i) {
      if (builtin_code == kTfLiteBuiltinAdd)
        output[i] = input_1[i] + input_2[i];
      else
        output[i] = input_1[i] - input_2[i];
    }
    return kTfLiteOk;
  }

  // Holds the indices of the input/output tensors.
  // inputs_[i] is list of all input tensors to node at index 'i'.
  // outputs_[i] is list of all output tensors to node at index 'i'.
  std::vector<std::vector<int>> inputs_, outputs_;
  // Holds the builtin code of the ops.
  // builtin_code_[i] is the type of node at index 'i'
  std::vector<int> builtin_code_;
};


Сравните и оцените нового делегата

В TFLite есть набор инструментов, которые можно быстро протестировать на модели TFLite.

  • Model Benchmark Tool : Инструмент берет модель TFLite, генерирует случайные входные данные, а затем многократно запускает модель в течение заданного количества прогонов. В конце он печатает сводную статистику задержки.
  • Inference Diff Tool : для заданной модели инструмент генерирует случайные гауссовские данные и передает их через два разных интерпретатора TFLite, один из которых работает с однопоточным ядром ЦП, а другой использует определяемую пользователем спецификацию. Он измеряет абсолютную разницу между выходными тензорами каждого интерпретатора для каждого элемента. Этот инструмент также может быть полезен для отладки проблем с точностью.
  • Существуют также специальные инструменты оценки для классификации изображений и обнаружения объектов. Эти инструменты можно найти здесь

Кроме того, TFLite имеет большой набор тестов ядра и операционных модулей, которые можно повторно использовать для тестирования нового делегата с большим охватом и для обеспечения того, чтобы обычный путь выполнения TFLite не был нарушен.

Чтобы добиться повторного использования тестов и инструментов TFLite для нового делегата, вы можете использовать любой из следующих двух вариантов:

Выбор наилучшего подхода

Оба подхода требуют некоторых изменений, как подробно описано ниже. Однако первый подход связывает делегат статически и требует перестройки инструментов тестирования, сравнительного анализа и оценки. Напротив, второй делает делегата общей библиотекой и требует, чтобы вы предоставили методы создания/удаления из общей библиотеки.

В результате механизм внешнего делегата будет работать с готовыми двоичными файлами инструментов TFLite Tensorflow Lite . Но он менее явный, и его может быть сложнее настроить в автоматизированных интеграционных тестах. Используйте подход делегированного регистратора для большей ясности.

Вариант 1. Используйте делегированного регистратора

Регистратор делегатов хранит список поставщиков делегатов, каждый из которых обеспечивает простой способ создания делегатов TFLite на основе флагов командной строки и, следовательно, удобен для инструментов. Чтобы подключить нового делегата ко всем упомянутым выше инструментам Tensorflow Lite, вы сначала создаете новый поставщик делегатов, подобный этому , а затем вносите лишь несколько изменений в правила BUILD. Полный пример этого процесса интеграции показан ниже (а код можно найти здесь ).

Предполагая, что у вас есть делегат, который реализует API-интерфейсы SimpleDelegate и внешние API-интерфейсы «C» для создания/удаления этого «фиктивного» делегата, как показано ниже:

// Returns default options for DummyDelegate.
DummyDelegateOptions TfLiteDummyDelegateOptionsDefault();

// Creates a new delegate instance that need to be destroyed with
// `TfLiteDummyDelegateDelete` when delegate is no longer used by TFLite.
// When `options` is set to `nullptr`, the above default values are used:
TfLiteDelegate* TfLiteDummyDelegateCreate(const DummyDelegateOptions* options);

// Destroys a delegate created with `TfLiteDummyDelegateCreate` call.
void TfLiteDummyDelegateDelete(TfLiteDelegate* delegate);

Чтобы интегрировать «DummyDelegate» с Benchmark Tool и Inference Tool, определите DelegateProvider, как показано ниже:

class DummyDelegateProvider : public DelegateProvider {
 public:
  DummyDelegateProvider() {
    default_params_.AddParam("use_dummy_delegate",
                             ToolParam::Create<bool>(false));
  }

  std::vector<Flag> CreateFlags(ToolParams* params) const final;

  void LogParams(const ToolParams& params) const final;

  TfLiteDelegatePtr CreateTfLiteDelegate(const ToolParams& params) const final;

  std::string GetName() const final { return "DummyDelegate"; }
};
REGISTER_DELEGATE_PROVIDER(DummyDelegateProvider);

std::vector<Flag> DummyDelegateProvider::CreateFlags(ToolParams* params) const {
  std::vector<Flag> flags = {CreateFlag<bool>("use_dummy_delegate", params,
                                              "use the dummy delegate.")};
  return flags;
}

void DummyDelegateProvider::LogParams(const ToolParams& params) const {
  TFLITE_LOG(INFO) << "Use dummy test delegate : ["
                   << params.Get<bool>("use_dummy_delegate") << "]";
}

TfLiteDelegatePtr DummyDelegateProvider::CreateTfLiteDelegate(
    const ToolParams& params) const {
  if (params.Get<bool>("use_dummy_delegate")) {
    auto default_options = TfLiteDummyDelegateOptionsDefault();
    return TfLiteDummyDelegateCreateUnique(&default_options);
  }
  return TfLiteDelegatePtr(nullptr, [](TfLiteDelegate*) {});
}

Определения правила BUILD важны, так как вам нужно убедиться, что библиотека всегда связана и не удаляется оптимизатором.

#### The following are for using the dummy test delegate in TFLite tooling ####
cc_library(
    name = "dummy_delegate_provider",
    srcs = ["dummy_delegate_provider.cc"],
    copts = tflite_copts(),
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/tools/delegates:delegate_provider_hdr",
    ],
    alwayslink = 1, # This is required so the optimizer doesn't optimize the library away.
)

Теперь добавьте эти два правила-оболочки в свой файл BUILD, чтобы создать версию Benchmark Tool и Inference Tool, а также другие инструменты оценки, которые могут работать с вашим собственным делегатом.

cc_binary(
    name = "benchmark_model_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/benchmark:benchmark_model_main",
    ],
)

cc_binary(
    name = "inference_diff_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/inference_diff:run_eval_lib",
    ],
)

cc_binary(
    name = "imagenet_classification_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/imagenet_image_classification:run_eval_lib",
    ],
)

cc_binary(
    name = "coco_object_detection_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/coco_object_detection:run_eval_lib",
    ],
)

Вы также можете подключить этого поставщика делегатов к тестам ядра TFLite, как описано здесь .

Вариант 2. Использование внешнего делегата

В этом варианте вы сначала создаете внешний адаптер делегата external_delegate_adaptor.cc , как показано ниже. Обратите внимание, что этот подход несколько менее предпочтителен по сравнению с Вариантом 1, о котором говорилось выше .

TfLiteDelegate* CreateDummyDelegateFromOptions(char** options_keys,
                                               char** options_values,
                                               size_t num_options) {
  DummyDelegateOptions options = TfLiteDummyDelegateOptionsDefault();

  // Parse key-values options to DummyDelegateOptions.
  // You can achieve this by mimicking them as command-line flags.
  std::unique_ptr<const char*> argv =
      std::unique_ptr<const char*>(new const char*[num_options + 1]);
  constexpr char kDummyDelegateParsing[] = "dummy_delegate_parsing";
  argv.get()[0] = kDummyDelegateParsing;

  std::vector<std::string> option_args;
  option_args.reserve(num_options);
  for (int i = 0; i < num_options; ++i) {
    option_args.emplace_back("--");
    option_args.rbegin()->append(options_keys[i]);
    option_args.rbegin()->push_back('=');
    option_args.rbegin()->append(options_values[i]);
    argv.get()[i + 1] = option_args.rbegin()->c_str();
  }

  // Define command-line flags.
  // ...
  std::vector<tflite::Flag> flag_list = {
      tflite::Flag::CreateFlag(...),
      ...,
      tflite::Flag::CreateFlag(...),
  };

  int argc = num_options + 1;
  if (!tflite::Flags::Parse(&argc, argv.get(), flag_list)) {
    return nullptr;
  }

  return TfLiteDummyDelegateCreate(&options);
}

#ifdef __cplusplus
extern "C" {
#endif  // __cplusplus

// Defines two symbols that need to be exported to use the TFLite external
// delegate. See tensorflow/lite/delegates/external for details.
TFL_CAPI_EXPORT TfLiteDelegate* tflite_plugin_create_delegate(
    char** options_keys, char** options_values, size_t num_options,
    void (*report_error)(const char*)) {
  return tflite::tools::CreateDummyDelegateFromOptions(
      options_keys, options_values, num_options);
}

TFL_CAPI_EXPORT void tflite_plugin_destroy_delegate(TfLiteDelegate* delegate) {
  TfLiteDummyDelegateDelete(delegate);
}

#ifdef __cplusplus
}
#endif  // __cplusplus

Теперь создайте соответствующую цель BUILD для создания динамической библиотеки, как показано ниже:

cc_binary(
    name = "dummy_external_delegate.so",
    srcs = [
        "external_delegate_adaptor.cc",
    ],
    linkshared = 1,
    linkstatic = 1,
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/c:common",
        "//tensorflow/lite/tools:command_line_flags",
        "//tensorflow/lite/tools:logging",
    ],
)

После создания этого файла внешнего делегата .so вы можете создавать двоичные файлы или использовать готовые для запуска с новым делегатом, если двоичный файл связан с библиотекой external_delegate_provider , которая поддерживает флаги командной строки, как описано здесь . Примечание. Этот внешний поставщик делегатов уже связан с существующими двоичными файлами тестирования и инструментов.

Обратитесь к описаниям здесь для иллюстрации того, как сравнить фиктивного делегата с помощью этого подхода с внешним делегатом. Вы можете использовать аналогичные команды для инструментов тестирования и оценки, упомянутых ранее.

Стоит отметить, что внешний делегат — это соответствующая реализация C++ делегата в привязке Tensorflow Lite Python, как показано здесь . Таким образом, созданную здесь библиотеку динамических внешних адаптеров делегатов можно напрямую использовать с API-интерфейсами Tensorflow Lite Python.

Ресурсы

Операционные системы АРКА BINARY_NAME
линукс x86_64
рука
аарх64
Андроид рука
аарх64