Como implementar um delegado personalizado

Uma LiteRT Delegar: executar os modelos (parciais ou inteiros) em outro executor. Esse mecanismo pode aproveitar uma variedade de aceleradores no dispositivo, como GPU ou Edge TPU (Tensor unidade de processamento) para inferência. Isso oferece aos desenvolvedores uma experiência 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?

A LiteRT tem uma grande 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 sem suporte delegado existente.
  • Você tem um acelerador de hardware personalizado que melhora o tempo de execução para diferentes.
  • Você está desenvolvendo otimizações de CPU (como a fusão de operadores) que podem acelerar certos 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 Média.

Gráfico original

Após aplicar esse "MyDelegate", o gráfico LiteRT original será atualizado da seguinte forma:

Gráfico com delegado

O gráfico acima é obtido quando o LiteRT divide o gráfico original duas regras a seguir:

  • Operações específicas que podem ser manipuladas pelo delegado são colocadas em um ao mesmo tempo que atende ao fluxo de trabalho de computação original dependências entre as operações.
  • Cada partição a ser delegada tem apenas nós de entrada e saída que não são e gerenciados pelo delegado.

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

Dependendo do modelo, o gráfico final pode ter um ou mais nós, último, o que significa que algumas operações não são suportadas pelo delegado. Em geral, você você não quer que várias partições sejam gerenciadas pelo delegado, sempre que você alterna de delegado para o gráfico principal, há uma sobrecarga para passando os resultados do subgráfico delegado para o gráfico principal que resulta devido a cópias de memória (por exemplo, GPU para CPU). Essa sobrecarga pode compensar Ganhos de desempenho, especialmente 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 API SimpleDelegate.

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

1 – SimpleDelegateInterface

Esta classe representa os recursos do delegado, quais operações são com suporte e uma classe de fábrica para criar um kernel que encapsula os 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 e executar o delegada.

Ele tem: (Consulte definição)

  • Init(...): que será chamado uma vez para fazer qualquer inicialização única.
  • Prepare(...): chamado para cada instância diferente desse nó. Isso acontece caso você tenha várias partições delegadas. Normalmente, você quer que a memória alocações aqui, já que ele será chamado sempre que os tensores forem redimensionados.
  • Intent(...): que será chamado para inferência.

Exemplo

Neste exemplo, você vai criar um delegado muito simples que dá suporte apenas a dois tipos de operações (ADD) e (SUB) apenas 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_;
};

Fazer um comparativo e avaliar o novo delegado

O TFLite tem um conjunto de ferramentas que podem ser testadas rapidamente em um modelo desse app.

  • Ferramenta de comparativo de mercado de modelos: A ferramenta usa um modelo do TFLite, gera entradas aleatórias e depois executa o modelo em um número especificado de execuções. Ele imprime a latência agregada estatísticas no final.
  • Ferramenta de comparação de inferências: Para um determinado modelo, a ferramenta gera dados gaussianos aleatórios e os transmite usando dois intérpretes de TFLite diferentes, um que executa uma CPU com um único thread kernel e o outro usando uma especificação definida pelo usuário. Ela mede o diferença entre os tensores de saída de cada intérprete, em uma por elemento. Essa ferramenta também pode ser útil para depurar a acurácia problemas.
  • Há também ferramentas de avaliação específicas de tarefas, para classificação de imagens e detecção de objetos. Essas ferramentas estão disponíveis aqui

Além disso, o TFLite tem um grande conjunto de testes de unidade operacional e kernel que podem ser reutilizado para testar o novo delegado com mais cobertura e para garantir que O caminho de execução do TFLite não está corrompido.

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

Como escolher a melhor abordagem

As duas abordagens exigem algumas mudanças, conforme detalhado abaixo. No entanto, a primeira vincula o delegado estaticamente e requer a reconstrução do teste, ferramentas de avaliação e comparativos de mercado. Já o segundo faz a de acesso como uma biblioteca compartilhada e exige que você exponha a função criar/excluir métodos da biblioteca compartilhada.

Como resultado, o mecanismo delegado externo funcionará com os atributos binários pré-criados de ferramentas LiteRT. Mas ela é menos explícita e pode ser mais complicado de configurar em sistemas testes de integração. Use a abordagem de registrador delegado para ter mais clareza.

Opção 1: aproveitar o registrador delegado

A delegar registrador mantém uma lista de provedores delegados, e cada um deles fornece uma maneira fácil de criar O TFLite delega delegados com base em sinalizações de linha de comando e, portanto, é conveniente para ou de terceiros. Para conectar o novo delegado a todas as ferramentas LiteRT mencionadas acima, crie um novo provedor delegado, e fazer algumas mudanças nas regras BUILD. Um exemplo completo disso o 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 o externo "C" APIs de criação/exclusão deste "fictício" delegado, como 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” à ferramenta de comparação e à 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 porque você precisa garantir é 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 do ferramenta de comparativo de mercado e 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 do kernel do TFLite, conforme descrito aqui.

Opção 2: aproveitar o delegado externo

Nesta alternativa, primeiro você cria um adaptador externo delegado external_delegate_adaptor.cc conforme mostrado abaixo. Essa abordagem é um pouco menos preferida do que Opção 1, conforme mencionado acima.

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

Após a criação do arquivo .so delegado externo, você pode compilar binários ou usar pré-criados para execução com o novo delegado, desde que o binário esteja vinculado com as external_delegate_provider que oferece suporte a sinalizações de linha de comando, conforme descrito aqui. Observação: este provedor delegado externo já foi vinculado a testes e ferramentas.

Consulte as descrições aqui para ver uma ilustração de como comparar o delegado fictício com este delegada externa. É possível usar comandos semelhantes para o teste e das ferramentas de avaliação mencionadas anteriormente.

O delegate externo é o código C++ correspondente implementação de delegate na vinculação LiteRT Python, conforme mostrado. aqui. Portanto, a biblioteca de adaptador de delegado externo dinâmico criada aqui pode ser usada diretamente com as APIs LiteRT Python.

Recursos

SO ARCH BINARY_NAME
Linux x86_64
arm
aarch64
Android arm
aarch64