Implementazione di un delegato personalizzato

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

Il diagramma seguente riassume i delegati. Ulteriori dettagli nelle sezioni seguenti.

Delegati TFLite

Quando devo creare un delegato personalizzato?

TensorFlow Lite ha un'ampia varietà di delegati per acceleratori di destinazione come GPU, DSP ed EdgeTPU.

La creazione di un delegato è utile nei seguenti scenari:

  • Vuoi integrare un nuovo motore di inferenza ML non supportato da alcun delegato esistente.
  • Disponi di un acceleratore hardware personalizzato che migliora il runtime per gli scenari noti.
  • Stai sviluppando ottimizzazioni della CPU (come il fusione dell'operatore) in grado di accelerare determinati modelli.

Come lavorano i delegati?

Prendi in considerazione un semplice grafico del modello come il seguente e un delegato "MyDelega" che offre un'implementazione più rapida per le operazioni Conv2D e Mean.

Grafico originale

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

Grafico con delegato

Il grafico riportato sopra è ottenuto quando TensorFlow Lite suddivide il grafico originale in base a due regole:

  • Le operazioni specifiche che potrebbero essere gestite dal delegato vengono inserite in una partizione, soddisfacendo comunque le dipendenze del flusso di lavoro di calcolo originali tra le operazioni.
  • Ogni partizione da delegare include solo nodi di input e di output non gestiti dal delegato.

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

A seconda del modello, il grafico finale può avere uno o più nodi. Questo significa che alcune operazioni non sono supportate dal delegato. In generale, non vuoi che il delegato possa gestire più partizioni, perché ogni volta che passi da delegato al grafico principale, si verifica un sovraccarico per il passaggio dei risultati dal sottografico delegato al grafico principale a causa di copie in memoria (ad esempio, da GPU a CPU). Tale overhead potrebbe compensare i miglioramenti in termini di prestazioni, soprattutto in presenza di una grande quantità di copie in memoria.

Implementazione di un delegato personalizzato

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

Per creare un nuovo delegato, devi implementare due interfacce e fornire la tua implementazione per i metodi dell'interfaccia.

1 - SimpleDelegateInterface

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

2 - SimpleDelegateKernelInterface

Questa classe incapsula la logica per l'inizializzazione, la preparazione e l'esecuzione della partizione delegata.

Contiene: (vedi definizione)

  • Init(...): che verrà chiamata una volta per eseguire qualsiasi inizializzazione una tantum.
  • Preparati(...): richiamato per ogni istanza diversa di questo nodo. Questo accade se hai più partizioni delegate. Di solito devi eseguire allocazioni di memoria qui, poiché questa verrà chiamata 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 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>();
  }
};

A questo punto, crea il tuo kernel delegato ereditandolo dal comando 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 benchmark dei modelli: lo strumento prende un modello TFLite, genera input casuali e poi esegue ripetutamente il modello per un numero specificato di esecuzioni. Alla fine stampa statistiche di latenza aggregate.
  • Strumento di differenza dell'inferenza: per un determinato modello, lo strumento genera dati gaussiani casuali e li passa a due diversi interpreti TFLite, uno in esecuzione con un singolo kernel della CPU con thread e l'altro utilizzando una specifica definita dall'utente. Misura la differenza assoluta tra i tensori di output di ciascun interprete, in base al singolo elemento. Questo strumento può essere utile anche per eseguire il debug di problemi di accuratezza.
  • Esistono anche strumenti di valutazione specifici per le attività, per la classificazione delle immagini e il rilevamento di oggetti. Questi strumenti sono disponibili qui

Inoltre, TFLite ha un ampio set di test del kernel e delle unità operative che potrebbe essere riutilizzato per testare il nuovo delegato con più copertura e per garantire che il normale percorso di esecuzione di TFLite non venga interrotto.

Per poter 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 ricreazione degli strumenti di test, analisi del benchmark e valutazione. Al contrario, la seconda rende il delegato come libreria condivisa e richiede l'esposizione dei metodi di creazione/eliminazione dalla libreria condivisa.

Di conseguenza, il meccanismo di delega esterna funzionerà con i programmi binari predefiniti di TFLite per gli strumenti Tensorflow Lite. Tuttavia, è meno esplicita e potrebbe essere più complicato da configurare nei test di integrazione automatizzati. Utilizza l'approccio del registrar del delegato per maggiore chiarezza.

Opzione 1: sfrutta il registrar delegato

Il registrar del delegato mantiene un elenco di provider delegati, ognuno dei quali offre un modo semplice per creare delegati TFLite in base ai flag della riga di comando e, di conseguenza, pratici per la creazione di strumenti. Per collegare il nuovo delegato a tutti gli strumenti Tensorflow Lite menzionati in precedenza, devi prima creare un nuovo provider delegato, quindi apportare solo alcune modifiche alle regole COSTRUIRE. Di seguito è riportato un esempio completo di questo processo di integrazione (e il codice è disponibile qui).

Supponendo che tu abbia un delegato che implementa le API SimpleDelega e le API "C" esterne per la creazione o l'eliminazione di 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, definisci un DelegaProvider come indicato 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 perché devi assicurarti che la libreria sia sempre collegata e non venga 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 file Build per creare una versione dello strumento di benchmark e dello strumento di inferenza e altri strumenti di valutazione da eseguire 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: fai affidamento su un delegato esterno

In questa alternativa, devi prima creare un adattatore del delegato esterno, external_delegate_adaptor.cc, come mostrato di seguito. Nota che questo approccio è leggermente meno preferito rispetto all'Opzione 1, come precitato.

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 il file .so del delegato esterno, puoi creare programmi binari o utilizzarne di predefiniti da eseguire con il nuovo delegato, purché il programma 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 a programmi binari di test e strumenti esistenti.

Consulta le descrizioni qui per un'illustrazione di come eseguire il benchmark del delegato fittizio tramite questo approccio del delegato esterno. Puoi usare comandi simili per gli strumenti di test e valutazione menzionati in precedenza.

Vale la pena notare che il delegato esterno è l'implementazione C++ corrispondente del delegato nell'associazione Python in Tensorflow Lite, come mostrato qui. Pertanto, la libreria di adattatori esterni dinamici creata qui potrebbe essere utilizzata direttamente con le API Python Tensorflow Lite.

Risorse

Sistema operativo ARCH BINARY_NAME
Linux x86_64
gruppo
aarch64
Android gruppo
aarch64