Grazie per esserti sintonizzato su Google I/O. Visualizza tutte le sessioni su richiesta Guarda su richiesta

Implementazione di un delegato personalizzato

Che cos'è un delegato TensorFlow Lite?

Un delegato TensorFlow Lite ti consente di eseguire i tuoi modelli (parte o intero) su un altro executor. Questo meccanismo può sfruttare una varietà di acceleratori sul dispositivo come la GPU o l'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 devo creare un delegato personalizzato?

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

La creazione del proprio delegato è utile nei seguenti scenari:

  • Si desidera integrare un nuovo motore di inferenza ML non supportato da alcun 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 determinati 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 TensorFlow Lite originale verrà aggiornato come segue:

Graph with delegate

Il grafico sopra si ottiene quando TensorFlow Lite divide il grafico originale seguendo due regole:

  • Le operazioni specifiche che potrebbero essere gestite dal delegato vengono inserite in una partizione pur continuando a soddisfare le dipendenze del flusso di lavoro di elaborazione originale tra le operazioni.
  • Ogni 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 alla sua chiamata di chiamata.

A seconda del modello, il grafico finale può finire 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 è presente una grande quantità di copie di memoria.

Implementazione del proprio delegato personalizzato

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

Per creare un nuovo delegato, devi implementare 2 interfacce e fornire la tua implementazione per i metodi di interfaccia.

1 - SimpleDelegateInterface

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

2 - SimpleDelegateKernelInterface

Questa classe incapsula la logica per inizializzare/preparare/e eseguire la partizione delegata.

Ha: (Vedi definizione )

  • Init(...): che verrà chiamato una volta per eseguire qualsiasi inizializzazione una tantum.
  • Prepare(...): chiamato per ogni diversa istanza di questo nodo - questo accade se hai più partizioni delegate. Di solito si desidera eseguire allocazioni di memoria qui, poiché verrà chiamato ogni volta che i tensori vengono ridimensionati.
  • Invoke(...): che sarà 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 i 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>();
  }
};

Quindi, 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 ha una serie di strumenti che puoi testare rapidamente rispetto a un modello TFLite.

  • Strumento di benchmark 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 di latenza aggregate.
  • Strumento Inference Diff : per un dato modello, lo strumento genera dati gaussiani casuali e li passa attraverso due diversi interpreti TFLite, uno che esegue un kernel CPU a thread singolo e l'altro utilizzando una specifica definita dall'utente. Misura la differenza assoluta tra i tensori di uscita di ciascun interprete, in base all'elemento. 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'ampia serie 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 ottenere il riutilizzo dei test e degli 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 configurare nei test di integrazione automatizzati. Utilizzare l'approccio del registrar delegato per una maggiore chiarezza.

Opzione 1: sfrutta il registrar delegato

Il registrar delegato conserva un elenco di provider delegati, ognuno dei quali fornisce un modo semplice per creare delegati TFLite in base ai flag della riga di comando e sono quindi convenienti per gli strumenti. Per collegare il nuovo delegato a tutti gli strumenti Tensorflow Lite sopra menzionati, devi prima creare un nuovo provider delegato come questo e quindi apportare solo alcune modifiche alle regole BUILD. Un esempio completo di questo processo di integrazione è mostrato di seguito (e 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 Benchmark Tool e Inference Tool, definisci 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 in quanto è 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 potrebbero 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, devi prima creare un adattatore delegato esterno, external_delegate_adaptor.cc , come mostrato di seguito. Si noti che questo approccio è leggermente meno preferito rispetto all'Opzione 1, come è stato menzionato sopra .

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 la destinazione 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 delegato esterno, è possibile creare file binari o utilizzare quelli predefiniti 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 delegato esterno è già stato collegato ai binari di test e strumenti esistenti.

Fare riferimento alle descrizioni qui per un'illustrazione di come confrontare il delegato fittizio tramite questo approccio di 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 può essere utilizzata direttamente con le API Python di Tensorflow Lite.

Risorse

Sistema operativo ARCO BINARY_NAME
Linux x86_64
braccio
aarch64
Androide braccio
aarch64