Google I/O — это обертка! Наверстать упущенное в сеансах TensorFlow Просмотреть сеансы

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

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

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

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

TFLite Delegates

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

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

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

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

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

Рассмотрим простой граф модели, такой как следующий, и делегат 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.

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

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

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

Выбираем лучший подход

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

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

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

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

Предположим, у вас есть делегат, который реализует 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 ++ делегата в привязке Python Tensorflow Lite, как показано здесь . Таким образом, созданную здесь динамическую библиотеку адаптера внешнего делегата можно напрямую использовать с API-интерфейсами Python Tensorflow Lite.

Ресурсы

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