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

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

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

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

TFLite Delegates

Когда мне следует создать собственный делегат?

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

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

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

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

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

Original graph

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

Graph with delegate

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

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

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

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

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

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

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

1 — SimpleDelegateInterface

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

2 — SimpleDelegateKernelInterface

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

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

  • Init(...): который будет вызываться один раз для однократной инициализации.
  • Подготовка(...): вызывается для каждого отдельного экземпляра этого узла — это происходит, если у вас есть несколько делегированных разделов. Обычно вы хотите выделить здесь память, так как это будет вызываться каждый раз при изменении размера тензоров.
  • 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.

  • Инструмент оценки модели : инструмент берет модель TFLite, генерирует случайные входные данные, а затем повторно запускает модель для указанного количества прогонов. В конце он печатает агрегированную статистику задержки.
  • Инструмент Inference Diff : для данной модели инструмент генерирует случайные гауссовы данные и передает их через два разных интерпретатора 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 и Inference, определите 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-интерфейсами Python Tensorflow Lite.

Ресурсы

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