Implementazione di un delegato personalizzato

Cos'è un delegato TensorFlow Lite?

Un delegato TensorFlow Lite ti consente di eseguire i tuoi modelli (parte o intero) su un altro esecutore. Questo meccanismo può sfruttare una varietà di acceleratori sul dispositivo come GPU o Edge TPU (Tensor Processing Unit) per l'inferenza. Ciò fornisce agli sviluppatori un metodo flessibile e disaccoppiato dal TFLite predefinito per accelerare l'inferenza.

Il diagramma seguente riassume i delegati, maggiori dettagli nelle sezioni seguenti.

TFLite Delegates

Quando dovrei creare un delegato personalizzato?

TensorFlow Lite dispone di un'ampia varietà di delegati per acceleratori target come GPU, DSP, EdgeTPU e framework come Android NNAPI.

La creazione del proprio delegato è utile nei seguenti scenari:

  • Vuoi integrare un nuovo motore di inferenza ML non supportato da nessun delegato esistente.
  • Hai un acceleratore hardware personalizzato che migliora il runtime per scenari noti.
  • Stai sviluppando ottimizzazioni della CPU (come la fusione dell'operatore) che possono velocizzare alcuni modelli.

Come funzionano i delegati?

Considera un semplice modello grafico come il seguente e un delegato "MyDelegate" che ha un'implementazione più rapida per le operazioni Conv2D e Mean.

Original graph

Dopo aver applicato questo "MyDelegate", il grafico originale di TensorFlow Lite verrà aggiornato come segue:

Graph with delegate

Il grafico sopra è ottenuto poiché TensorFlow Lite divide il grafico originale seguendo due regole:

  • Le operazioni specifiche che potrebbero essere gestite dal delegato vengono inserite in una partizione pur soddisfacendo le dipendenze del flusso di lavoro di elaborazione originale tra le operazioni.
  • Ciascuna partizione da delegare ha solo nodi di input e output che non sono gestiti dal delegato.

Ogni partizione gestita da un delegato viene sostituita da un nodo delegato (può anche essere chiamato come kernel delegato) nel grafico originale che valuta la partizione durante la chiamata di invocazione.

A seconda del modello, il grafico finale può terminare con uno o più nodi, quest'ultimo significa che alcune operazioni non sono supportate dal delegato. In generale, non vuoi avere più partizioni gestite dal delegato, perché ogni volta che passi dal delegato al grafico principale, c'è un sovraccarico per passare i risultati dal sottografo delegato al grafico principale che risulta a causa della memoria copie (ad esempio, da GPU a CPU). Tale sovraccarico potrebbe compensare i miglioramenti delle prestazioni, soprattutto quando sono presenti grandi quantità di copie di memoria.

Implementazione del proprio delegato personalizzato

Il metodo preferito per aggiungere un delegato è utilizzare l'API SimpleDelegate .

Per creare un nuovo delegato, è necessario implementare 2 interfacce e fornire la propria implementazione per i metodi di interfaccia.

1 - SimpleDelegateInterface

Questa classe rappresenta le capacità del delegato, quali operazioni sono supportate, e una classe factory per creare un kernel che incapsula il grafico delegato. Per ulteriori dettagli, consulta l'interfaccia definita in questo file di intestazione C++ . I commenti nel codice spiegano dettagliatamente ciascuna API.

2 - SimpleDelegateKernelInterface

Questa classe incapsula la logica per l'inizializzazione/preparazione/ed esecuzione della partizione delegata.

Ha: (Vedi definizione )

  • Init(...): che verrà chiamato una volta per eseguire qualsiasi inizializzazione una tantum.
  • Prepara(...): chiamato per ogni diversa istanza di questo nodo - questo accade se hai più partizioni delegate. Di solito vuoi fare le allocazioni di memoria qui, poiché questo verrà chiamato ogni volta che i tensori vengono ridimensionati.
  • Invoke(...): che verrà chiamato per l'inferenza.

Esempio

In questo esempio creerai un delegato molto semplice che può supportare solo 2 tipi di operazioni (ADD) e (SUB) solo con tensori 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>();
  }
};

Successivamente, crea il tuo kernel delegato ereditando da 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_;
};


Confronta e valuta il nuovo delegato

TFLite dispone di una serie di strumenti che puoi testare rapidamente rispetto a un modello TFLite.

  • Strumento di benchmarking del modello : lo strumento prende un modello TFLite, genera input casuali e quindi esegue ripetutamente il modello per un numero specificato di esecuzioni. Alla fine stampa le statistiche aggregate sulla latenza.
  • Strumento Diff di inferenza : per un dato modello, lo strumento genera dati gaussiani casuali e li passa attraverso due diversi interpreti TFLite, uno che esegue il kernel della CPU a thread singolo e l'altro che utilizza una specifica definita dall'utente. Misura la differenza assoluta tra i tensori di output di ciascun interprete, in base agli elementi. Questo strumento può essere utile anche per il debug di problemi di precisione.
  • Esistono anche strumenti di valutazione specifici per attività, per la classificazione delle immagini e il rilevamento degli oggetti. Questi strumenti possono essere trovati qui

Inoltre, TFLite dispone di un ampio set di test del kernel e delle unità operative che potrebbero essere riutilizzati per testare il nuovo delegato con maggiore copertura e per garantire che il normale percorso di esecuzione di TFLite non venga interrotto.

Per riutilizzare i test e gli strumenti TFLite per il nuovo delegato, puoi utilizzare una delle due opzioni seguenti:

Scegliere l'approccio migliore

Entrambi gli approcci richiedono alcune modifiche come descritto di seguito. Tuttavia, il primo approccio collega il delegato in modo statico e richiede la ricostruzione degli strumenti di test, benchmarking e valutazione. Al contrario, il secondo rende il delegato come una libreria condivisa e richiede di esporre i metodi di creazione/eliminazione dalla libreria condivisa.

Di conseguenza, il meccanismo del delegato esterno funzionerà con i binari degli strumenti Tensorflow Lite predefiniti di TFLite. Ma è meno esplicito e potrebbe essere più complicato da impostare nei test di integrazione automatizzati. Utilizza l'approccio del registrar delegato per una maggiore chiarezza.

Opzione 1: sfruttare il registrar delegato

Il registrar dei delegati mantiene un elenco di provider di delegati, ognuno dei quali fornisce un modo semplice per creare delegati TFLite in base a flag della riga di comando e sono quindi utili per gli strumenti. Per collegare il nuovo delegato a tutti gli strumenti Tensorflow Lite menzionati sopra, crea prima un nuovo provider di delegati come questo e poi apporta solo alcune modifiche alle regole BUILD. Di seguito è mostrato un esempio completo di questo processo di integrazione (il codice può essere trovato qui ).

Supponendo che tu abbia un delegato che implementa le API SimpleDelegate e le API "C" esterne per creare/eliminare questo delegato "fittizio" come mostrato di seguito:

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

Per integrare "DummyDelegate" con lo strumento di benchmark e lo strumento di inferenza, definire un DelegateProvider come di seguito:

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

Le definizioni delle regole BUILD sono importanti poiché è necessario assicurarsi che la libreria sia sempre collegata e non eliminata dall'ottimizzatore.

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

Ora aggiungi queste due regole wrapper nel tuo file BUILD per creare una versione di Benchmark Tool e Inference Tool e altri strumenti di valutazione che possano essere eseguiti con il tuo delegato.

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

Puoi anche collegare questo provider delegato ai test del kernel TFLite come descritto qui .

Opzione 2: sfruttare il delegato esterno

In questa alternativa, crea prima un adattatore delegato esterno external_delegate_adaptor.cc come mostrato di seguito. Si noti che questo approccio è leggermente meno preferibile rispetto all'Opzione 1, come già menzionato .

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

Ora crea il target BUILD corrispondente per creare una libreria dinamica come mostrato di seguito:

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

Dopo aver creato questo file .so del delegato esterno, è possibile creare file binari o utilizzarne di precostruiti da eseguire con il nuovo delegato purché il file binario sia collegato alla libreria external_delegate_provider che supporta i flag della riga di comando come descritto qui . Nota: questo provider di delegati esterni è già stato collegato ai file binari di test e strumenti esistenti.

Fare riferimento alle descrizioni qui per un'illustrazione di come confrontare il delegato fittizio tramite questo approccio del delegato esterno. È possibile utilizzare comandi simili per gli strumenti di test e valutazione menzionati in precedenza.

Vale la pena notare che il delegato esterno è la corrispondente implementazione C++ del delegato nell'associazione Tensorflow Lite Python come mostrato qui . Pertanto, la libreria dell'adattatore delegato esterno dinamico creata qui potrebbe essere utilizzata direttamente con le API Python Tensorflow Lite.

Risorse

sistema operativo ARCO BINARY_NAME
Linux x86_64
braccio
aarch64
Androide braccio
aarch64