O Dia da Comunidade de ML é dia 9 de novembro! Junte-nos para atualização de TensorFlow, JAX, e mais Saiba mais

Implementando um Delegado Personalizado

O que é um TensorFlow Lite Delegate?

Um TensorFlow Lite Delegate permite que você execute seus modelos (parte ou todo) em outro executor. Esse mecanismo pode aproveitar uma variedade de aceleradores no dispositivo, como a GPU ou Edge TPU (Unidade de Processamento de Tensor) para inferência. Isso fornece aos desenvolvedores um método flexível e desacoplado do TFLite padrão para acelerar a inferência.

O diagrama abaixo resume os delegados, mais detalhes nas seções abaixo.

TFLite Delegates

Quando devo criar um delegado personalizado?

O TensorFlow Lite tem uma grande variedade de delegados para aceleradores de destino, como GPU, DSP, EdgeTPU e estruturas como Android NNAPI.

Criar seu próprio delegado é útil nos seguintes cenários:

  • Você deseja integrar um novo mecanismo de inferência de ML não compatível com nenhum delegado existente.
  • Você tem um acelerador de hardware personalizado que melhora o tempo de execução para cenários conhecidos.
  • Você está desenvolvendo otimizações de CPU (como fusão do operador) que podem acelerar certos modelos.

Como funcionam os delegados?

Considere um gráfico de modelo simples, como o seguinte, e um delegado “MyDelegate” que tem uma implementação mais rápida para operações Conv2D e Média.

Original graph

Depois de aplicar este “MyDelegate”, o gráfico TensorFlow Lite original será atualizado da seguinte forma:

Graph with delegate

O gráfico acima é obtido conforme o TensorFlow Lite divide o gráfico original seguindo duas regras:

  • Operações específicas que podem ser manipuladas pelo delegado são colocadas em uma partição enquanto ainda satisfazem as dependências do fluxo de trabalho de computação original entre as operações.
  • Cada partição a ser delegada possui apenas nós de entrada e saída que não são manipulados pelo delegado.

Cada partição manipulada por um delegado é substituída por um nó delegado (também pode ser chamado como um kernel delegado) no gráfico original que avalia a partição em sua chamada de invocação.

Dependendo do modelo, o gráfico final pode terminar com um ou mais nós, o último significando que algumas operações não são suportadas pelo delegado. Em geral, você não quer ter várias partições gerenciadas pelo delegado, porque cada vez que você muda de delegado para o gráfico principal, há uma sobrecarga para passar os resultados do subgráfico delegado para o gráfico principal resultante devido à memória cópias (por exemplo, GPU para CPU). Essa sobrecarga pode compensar os ganhos de desempenho, especialmente quando há uma grande quantidade de cópias de memória.

Implementando seu próprio delegado personalizado

O método preferido para adicionar um delegado é usar a API SimpleDelegate .

Para criar um novo delegado, você precisa implementar 2 interfaces e fornecer sua própria implementação para os métodos de interface.

1 - SimpleDelegateInterface

Esta classe representa os recursos do delegado, quais operações são suportadas e uma classe de fábrica para a criação de um kernel que encapsula o gráfico delegado. Para obter mais detalhes, consulte a interface definida neste arquivo de cabeçalho C ++ . Os comentários no código explicam cada API em detalhes.

2 - SimpleDelegateKernelInterface

Esta classe encapsula a lógica para inicializar / preparar / e executar a partição delegada.

Tem: (Ver definição )

  • Init (...): que será chamado uma vez para fazer qualquer inicialização única.
  • Prepare (...): chamado para cada instância diferente deste nó - isso acontece se você tiver várias partições delegadas. Normalmente você deseja fazer alocações de memória aqui, já que isso será chamado toda vez que os tensores forem redimensionados.
  • Invocar (...): que será chamado para inferência.

Exemplo

Neste exemplo, você criará um delegado muito simples que pode suportar apenas 2 tipos de operações (ADD) e (SUB) com tensores float32 apenas.

// 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>();
  }
};

Em seguida, crie seu próprio kernel delegado herdando de 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_;
};


Compare e avalie o novo delegado

O TFLite possui um conjunto de ferramentas que você pode testar rapidamente em um modelo TFLite.

  • Ferramenta de referência de modelo : a ferramenta pega um modelo TFLite, gera entradas aleatórias e, em seguida, executa repetidamente o modelo para um número especificado de execuções. Ele imprime estatísticas de latência agregadas no final.
  • Ferramenta Inference Diff : Para um determinado modelo, a ferramenta gera dados Gaussianos aleatórios e os passa por dois interpretadores TFLite diferentes, um executando um kernel de CPU de thread único e o outro usando uma especificação definida pelo usuário. Ele mede a diferença absoluta entre os tensores de saída de cada interpretador, por elemento. Essa ferramenta também pode ser útil para depurar problemas de precisão.
  • Existem também ferramentas de avaliação de tarefas específicas, para classificação de imagens e detecção de objetos. Essas ferramentas podem ser encontradas aqui

Além disso, o TFLite tem um grande conjunto de testes de unidade de kernel e op que podem ser reutilizados para testar o novo delegado com mais cobertura e para garantir que o caminho de execução regular do TFLite não seja interrompido.

Para conseguir a reutilização de testes e ferramentas TFLite para o novo delegado, você pode usar uma das duas opções a seguir:

Escolhendo a melhor abordagem

Ambas as abordagens requerem algumas mudanças, conforme detalhado a seguir. No entanto, a primeira abordagem vincula o delegado estaticamente e requer a reconstrução das ferramentas de teste, benchmarking e avaliação. Em contraste, o segundo torna o delegado como uma biblioteca compartilhada e requer que você exponha os métodos de criação / exclusão da biblioteca compartilhada.

Como resultado, o mecanismo de delegado externo funcionará com os binários de ferramentas Tensorflow Lite pré-construídos da TFLite . Mas é menos explícito e pode ser mais complicado de configurar em testes de integração automatizados. Use a abordagem do registrador delegado para maior clareza.

Opção 1: Aproveite o registrador delegado

O registrador de delegados mantém uma lista de provedores de delegados, cada um dos quais fornece uma maneira fácil de criar delegados TFLite com base em sinalizadores de linha de comando e, portanto, são convenientes para ferramentas. Para ligar o novo delegado para todas as ferramentas Tensorflow Lite mencionado acima, você primeiro criar um novo provedor delegado como este um , e, em seguida, faz apenas algumas alterações às regras de construção. Um exemplo completo desse processo de integração é mostrado abaixo (e o código pode ser encontrado aqui ).

Supondo que você tenha um delegado que implemente as APIs SimpleDelegate e as APIs "C" externas para criar / excluir esse delegado 'fictício', conforme mostrado abaixo:

// 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);

Para integrar o “DummyDelegate” com a ferramenta Benchmark e a Ferramenta de inferência, defina um DelegateProvider como abaixo:

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*) {});
}

As definições da regra BUILD são importantes, pois você precisa ter certeza de que a biblioteca está sempre vinculada e não descartada pelo otimizador.

#### 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.
)

Agora adicione essas duas regras de wrapper em seu arquivo BUILD para criar uma versão da Ferramenta de Benchmark e da Ferramenta de Inferência, e outras ferramentas de avaliação, que podem ser executadas com seu próprio delegado.

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",
    ],
)

Você também pode conectar esse provedor delegado aos testes de kernel TFLite, conforme descrito aqui .

Opção 2: alavancar delegado externo

Nesta alternativa, você primeiro cria um adaptador de delegado externo, o external_delegate_adaptor.cc, conforme mostrado abaixo. Observe que esta abordagem é um pouco menos preferida em comparação com a Opção 1, conforme mencionado anteriormente .

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

Agora crie o destino BUILD correspondente para construir uma biblioteca dinâmica, conforme mostrado abaixo:

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",
    ],
)

Depois que esse arquivo .so de delegado externo é criado, você pode construir binários ou usar os pré-construídos para executar com o novo delegado, desde que o binário esteja vinculado à biblioteca external_delegate_provider que suporta sinalizadores de linha de comando conforme descrito aqui . Observação: este provedor de delegado externo já foi vinculado aos binários de teste e ferramentas existentes.

Consulte as descrições aqui para obter uma ilustração de como comparar o delegado fictício por meio dessa abordagem de delegado externo. Você pode usar comandos semelhantes para as ferramentas de teste e avaliação mencionadas anteriormente.

É importante observar que o delegado externo é a implementação C ++ correspondente do delegado na vinculação Tensorflow Lite Python, conforme mostrado aqui . Portanto, a biblioteca do adaptador de delegado externo dinâmico criada aqui pode ser usada diretamente com as APIs Python do Tensorflow Lite.

Recursos

SO ARCO BINARY_NAME
Linux x86_64
braço
aarch64
Android braço
aarch64