Implementazione di un delegato personalizzato

LiteRT L'opzione Delega ti consente di: eseguire i tuoi modelli (in parte o per intero) su un altro esecutore. Questo meccanismo può sfruttare una serie di acceleratori on-device come GPU o Edge TPU (Tensor di elaborazione) per l'inferenza. Questo offre agli sviluppatori un ambiente disaccoppiato dal TFLite predefinito per velocizzare l'inferenza.

Il diagramma seguente riassume i delegati. Per ulteriori dettagli, consulta le sezioni di seguito.

Delegati TFLite

Quando devo creare un delegato personalizzato?

LiteRT ha un'ampia varietà di delegati per gli acceleratori target come GPU, DSP ed EdgeTPU.

Creare il proprio delegato è utile nei seguenti scenari:

  • Vuoi integrare un nuovo motore di inferenza ML non supportato da nessuna delegato esistente.
  • Hai un acceleratore hardware personalizzato che migliora il runtime per diversi scenari.
  • Stai sviluppando ottimizzazioni della CPU (ad esempio la fusione degli operatori) che possono per velocizzare alcuni modelli.

Come funzionano i delegati?

Considera un semplice grafico del modello come il seguente e un delegato "MyDelegate" con un'implementazione più rapida per le operazioni Conv2D e Media.

Grafico originale

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

Grafico con delegato

Il grafico sopra si ottiene man mano che LiteRT divide il grafico originale le due regole seguenti:

  • Le operazioni specifiche che possono essere gestite dal delegato vengono inserite in una il deployment, pur soddisfando il flusso di lavoro di calcolo originale le dipendenze tra le operazioni.
  • Ogni partizione da delegare contiene solo nodi di input e di output che non sono gestite dal delegato.

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

A seconda del modello, il grafo finale può presentare uno o più nodi, la seconda, ovvero alcune operazioni non sono supportate dal delegato. In generale, non vuoi che il delegato gestisca più partizioni, dato che ogni quando passi dalla delega al grafico principale, c'è un overhead per passaggio dei risultati dal sottografo delegato al grafico principale che restituisce a causa delle copie di memoria (ad esempio, da GPU a CPU). Un tale overhead potrebbe compensare di prestazioni migliori, soprattutto con una grande quantità di copie della memoria.

Implementazione del proprio delegato personalizzato

Il metodo preferito per aggiungere un delegato consiste nell'utilizzare API SimpleDelega.

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

1 - SimpleDelegateInterface

Questa classe rappresenta le capacità del delegato, ovvero le operazioni supportata, mentre una classe di fabbrica per la creazione di un kernel che incapsula grafico delegato. Per ulteriori dettagli, consulta l'interfaccia definita in questo File di intestazione C++. I commenti nel codice spiegano nel dettaglio ciascuna API.

2 - SimpleDelegateKernelInterface

Questa classe incapsula la logica per inizializzare, preparare, ed eseguire delegata.

Ha: (vedi definizione)

  • Init(...): che verrà chiamato una volta per eseguire qualsiasi inizializzazione una tantum.
  • Prepare(...): viene chiamato per ogni diversa istanza di questo nodo e succede se hai più partizioni delegati. In genere hai intenzione di usare allocazioni qui, poiché viene chiamata ogni volta che i tensori vengono ridimensionati.
  • Richiama(...): che verrà chiamato per l'inferenza.

Esempio

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

Quindi, crea il tuo kernel delegato ereditando dal 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 un set 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 esegue il modello per un determinato numero di esecuzioni. Stampa la latenza aggregata statistiche alla fine.
  • Strumento Diff inferenza: Per un determinato modello, lo strumento genera dati gaussiani casuali e li trasmette attraverso due diversi interpreti TFLite, una CPU con un singolo thread in esecuzione e l'altro usando una specifica definita dall'utente. Misura l'assoluta differenza tra i tensori di output di ciascun interprete, su un per singolo elemento. Questo strumento può essere utile anche per eseguire il debug dell'accuratezza che le applicazioni presentino problemi di prestazioni.
  • Esistono anche strumenti di valutazione specifici delle attività, per la classificazione delle immagini il rilevamento di oggetti. Questi strumenti sono disponibili qui

Inoltre, TFLite ha un ampio set di test del kernel e delle unità operative che riutilizzato per testare il nuovo delegato con una maggiore copertura e per garantire che Il percorso di esecuzione TFLite non è 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 collega il delegato in modo statico e richiede di ricreare il test, strumenti di benchmarking e valutazione. Al contrario, il secondo rende delegata come libreria condivisa e richiede la tua esposizione della libreria condivisa.

Di conseguenza, il meccanismo di delega esterno funzionerà con file binari predefiniti per strumenti LiteRT. Tuttavia, è meno esplicito e potrebbe essere più complicato configurarlo in test di integrazione. Per maggiore chiarezza, utilizza l'approccio del registrar delegato.

Opzione 1: utilizza il registrar delegato

La registrar delegato un elenco di provider delegati, ognuno dei quali fornisce un modo semplice per creare delegati TFLite basati su flag della riga di comando e sono quindi comodi e l'addestramento degli strumenti. Per collegare il nuovo delegato a tutti gli strumenti LiteRT menzionati qui sopra, devi prima creare un nuovo provider delegato, e poi apportare solo poche modifiche alle regole BUILD. Un esempio completo di integrazione è mostrato di seguito (e il codice è qui).

Supponendo che tu abbia un delegato che implementa le API SimpleDelegate e extern "C" API per la creazione/eliminazione di questo "fittizio" come indicato 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 il "DummyDelegate" con lo strumento di benchmark e lo strumento di inferenza, definisci un DelegaProvider come il seguente:

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 della regola BUILD sono importanti perché devi assicurarti che 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 Strumento di benchmark e inferenza e altri strumenti di valutazione che potrebbero 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: sfrutta il delegato esterno

In questa alternativa, devi prima creare un adattatore delegato esterno external_delegate_adaptor.cc come mostrato di seguito. Tieni presente che questo approccio è leggermente meno preferito rispetto a Opzione 1 come 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 il target BUILD corrispondente per creare una libreria dinamica come mostrato sotto:

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 delegato esterno, puoi creare programmi binari o utilizzare predefiniti da eseguire con il nuovo delegato, purché il file binario sia collegato il external_delegate_provider che supporta i flag della riga di comando, come descritto qui. Nota: questo fornitore delegato esterno è già stato collegato a uno esistente per testare e creare strumenti.

Consulta le descrizioni qui per un'illustrazione di come confrontare il delegato fittizio tramite questo l'approccio delegato esterno. Puoi usare comandi simili per le fasi di test di valutazione degli strumenti di valutazione citati in precedenza.

Vale la pena notare che il delegato esterno è il corrispondente C++ implementazione dell'associazione delegate in LiteRT Python come mostrato qui. Di conseguenza, la libreria di adattatore delegato esterno dinamica creata qui potrebbe essere utilizzate direttamente con le API Python LiteRT.

Risorse

Sistema operativo ARCH BINARY_NAME
Linux x86_64
gruppo
aarch64
Android gruppo
aarch64