Como implementar um delegado personalizado

Com um Delegate do TensorFlow Lite, é possível executar seus modelos (parte ou todo) em outro executor. Esse mecanismo pode aproveitar uma variedade de aceleradores no dispositivo, como a GPU ou a Edge TPU (Unidade de Processamento de Tensor, na sigla em inglês) 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. Confira mais detalhes nas seções abaixo.

Delegados do TFLite

Quando devo criar um delegado personalizado?

O TensorFlow Lite tem uma ampla variedade de delegados para aceleradores de destino, como GPU, DSP e EdgeTPU.

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

  • Você quer integrar um novo mecanismo de inferência de ML que não tem suporte de nenhum delegado atual.
  • Você tem um acelerador de hardware personalizado que melhora o ambiente de execução para cenários conhecidos.
  • Você está desenvolvendo otimizações de CPU, como fusão de operadores, que podem acelerar determinados modelos.

Como os delegados funcionam?

Considere um gráfico de modelo simples, como o mostrado a seguir, e um delegado "MyDelegate" que tem uma implementação mais rápida para as operações Conv2D e Mean.

Gráfico original

Depois de aplicar esse "MyDelegate", o gráfico original do TensorFlow Lite será atualizado da seguinte maneira:

Gráfico com delegado

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

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

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

Dependendo do modelo, o gráfico final pode ter um ou mais nós. Esse último significa que algumas operações não têm suporte do delegado. Em geral, não é recomendável que várias partições sejam processadas pelo delegado, porque cada vez que você alterna de delegado para o gráfico principal, há uma sobrecarga ao transmitir os resultados do subgráfico delegado para o gráfico principal, devido a cópias de memória (por exemplo, GPU para CPU). Essa sobrecarga pode compensar ganhos de desempenho quando há uma grande quantidade de cópias de memória.

Como implementar seu próprio delegado personalizado

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

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

1 – SimpleDelegateInterface

Essa classe representa os recursos do delegado, quais operações têm suporte e uma classe de fábrica para criar um kernel que encapsula o gráfico delegado. Para mais detalhes, consulte a interface definida neste arquivo C++ principal. Os comentários no código explicam cada API em detalhes.

2 – SimpleDelegateKernelInterface

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

Ele tem (consulte a definição):

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

Exemplo

Neste exemplo, você criará um delegado muito simples que é compatível apenas com dois tipos de operações (ADD) e (SUB) com tensores 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>();
  }
};

Em seguida, crie seu próprio kernel delegado herdando do 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 tem um conjunto de ferramentas que você pode testar rapidamente em relação a um modelo do TFLite.

  • Ferramenta de comparativo de mercado: a ferramenta usa um modelo TFLite, gera entradas aleatórias e executa o modelo repetidamente para um número especificado de execuções. Ele mostra estatísticas de latência agregadas no final.
  • Ferramenta de comparação de inferência: para um determinado modelo, a ferramenta gera dados gaussianos aleatórios e os transmite por dois intérpretes do TFLite diferentes, um que executa o kernel de CPU com thread único e o outro usa uma especificação definida pelo usuário. Ela mede a diferença absoluta entre os tensores de saída de cada intérprete, por elemento. Essa ferramenta também pode ser útil para depurar problemas de precisão.
  • Há também ferramentas de avaliação específicas para tarefas 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 operação que podem ser reutilizados para testar o novo delegado com mais cobertura e garantir que o caminho de execução normal do TFLite não seja corrompido.

Para reutilizar testes e ferramentas do TFLite para o novo delegado, é possível usar uma das duas opções a seguir:

Como escolher a melhor abordagem

Ambas as abordagens exigem algumas mudanças, conforme detalhado abaixo. No entanto, a primeira abordagem vincula o delegado estaticamente e requer a recriação das ferramentas de teste, comparação e avaliação. Por outro lado, na segunda, o delegado é uma biblioteca compartilhada e exige que você exponha os métodos de criação/exclusão da biblioteca compartilhada.

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

Opção 1: usar o registrador delegado

O registrador delegado mantém uma lista de provedores delegados. Cada um deles fornece uma maneira fácil de criar delegados do TFLite com base em sinalizações de linha de comando e, portanto, é conveniente para ferramentas. Para conectar o novo delegado a todas as ferramentas do Tensorflow Lite mencionadas acima, primeiro crie um novo provedor de delegado e, em seguida, faça apenas algumas alterações nas regras de BUILD. Um exemplo completo desse processo de integração é mostrado abaixo (e o código pode ser encontrado aqui).

Suponha que você tenha um delegado que implemente as APIs SimpleDelegate e as APIs "C" externas de criação/exclusão desse 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 as ferramentas de comparação e 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 porque você precisa garantir que a biblioteca seja sempre vinculada e não seja 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 ao arquivo BUILD para criar uma versão da ferramenta de comparação e da ferramenta de inferência, além de outras ferramentas de avaliação, que possam 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 do kernel do TFLite, conforme descrito aqui.

Opção 2: use o delegado externo

Como alternativa, primeiro você cria um adaptador delegado externo do external_delegate_adaptor.cc, como mostrado abaixo. Essa abordagem é um pouco menos recomendada em comparação com a Opção 1, como mencionado.

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 criar 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 o arquivo .so do delegado externo for criado, será possível criar binários ou usar os pré-criados para execução com o novo delegado, desde que o binário esteja vinculado à biblioteca external_delegate_provider, que oferece suporte às sinalizações de linha de comando, conforme descrito neste link. Observação: esse provedor delegado externo já foi vinculado a binários de teste e ferramentas.

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

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

Recursos

SO ARQUIVO BINARY_NAME
Linux x86_64
arm
aarch64
Android arm
aarch64