Implementieren eines benutzerdefinierten Delegaten

Was ist ein TensorFlow Lite-Delegierter?

Mit einem TensorFlow Lite- Delegaten können Sie Ihre Modelle (teilweise oder vollständig) auf einem anderen Executor ausführen. Dieser Mechanismus kann eine Vielzahl von Beschleunigern auf dem Gerät wie die GPU oder die Edge-TPU (Tensor Processing Unit) für Rückschlüsse nutzen. Dies bietet Entwicklern eine flexible und entkoppelte Methode von der Standard-TFLite, um die Inferenz zu beschleunigen.

Das folgende Diagramm fasst die Delegierten zusammen. Weitere Details finden Sie in den folgenden Abschnitten.

TFLite Delegates

Wann sollte ich einen benutzerdefinierten Delegaten erstellen?

TensorFlow Lite verfügt über eine Vielzahl von Delegaten für Zielbeschleuniger wie GPU, DSP, EdgeTPU und Frameworks wie Android NNAPI.

Das Erstellen eines eigenen Delegaten ist in den folgenden Szenarien hilfreich:

  • Sie möchten eine neue ML-Inferenz-Engine integrieren, die von keinem vorhandenen Delegaten unterstützt wird.
  • Sie haben einen benutzerdefinierten Hardwarebeschleuniger, der die Laufzeit für bekannte Szenarien verbessert.
  • Sie entwickeln CPU-Optimierungen (z. B. das Verschmelzen von Bedienern), mit denen bestimmte Modelle beschleunigt werden können.

Wie arbeiten Delegierte?

Stellen Sie sich ein einfaches Modelldiagramm wie das folgende und einen Delegaten „MyDelegate“ vor, der eine schnellere Implementierung für Conv2D- und Mean-Operationen bietet.

Original graph

Nach dem Anwenden dieses „MyDelegate“ wird das ursprüngliche TensorFlow Lite-Diagramm wie folgt aktualisiert:

Graph with delegate

Das obige Diagramm wird erhalten, wenn TensorFlow Lite das ursprüngliche Diagramm nach zwei Regeln aufteilt:

  • Bestimmte Vorgänge, die vom Delegaten ausgeführt werden könnten, werden in eine Partition gestellt, wobei die ursprünglichen Abhängigkeiten des Computerworkflows zwischen den Vorgängen weiterhin erfüllt werden.
  • Jede zu delegierende Partition verfügt nur über Eingabe- und Ausgabeknoten, die nicht vom Delegierten verarbeitet werden.

Jede Partition, die von einem Delegaten verwaltet wird, wird im ursprünglichen Diagramm durch einen Delegatenknoten ersetzt (kann auch als Delegatenkern bezeichnet werden), der die Partition bei ihrem Aufruf aufruft.

Je nach Modell kann das endgültige Diagramm einen oder mehrere Knoten enthalten. Letzterer bedeutet, dass einige Operationen vom Delegaten nicht unterstützt werden. Im Allgemeinen möchten Sie nicht, dass der Delegat mehrere Partitionen verwaltet, da jedes Mal, wenn Sie vom Delegaten zum Hauptdiagramm wechseln, ein Overhead für die Übergabe der Ergebnisse vom delegierten Untergraphen an das Hauptdiagramm entsteht, das sich aus dem Speicher ergibt Kopien (z. B. GPU zu CPU). Ein solcher Overhead kann Leistungssteigerungen ausgleichen, insbesondere wenn eine große Anzahl von Speicherkopien vorhanden ist.

Implementieren Ihres eigenen benutzerdefinierten Delegaten

Die bevorzugte Methode zum Hinzufügen eines Delegaten ist die Verwendung der SimpleDelegate-API .

Um einen neuen Delegaten zu erstellen, müssen Sie zwei Schnittstellen implementieren und Ihre eigene Implementierung für die Schnittstellenmethoden bereitstellen.

1 - SimpleDelegateInterface

Diese Klasse repräsentiert die Funktionen des Delegaten, welche Vorgänge unterstützt werden, und eine Factory-Klasse zum Erstellen eines Kernels, der das delegierte Diagramm kapselt. Weitere Informationen finden Sie in der in dieser C ++ - Headerdatei definierten Schnittstelle. Die Kommentare im Code erläutern jede API im Detail.

2 - SimpleDelegateKernelInterface

Diese Klasse kapselt die Logik zum Initialisieren / Vorbereiten / und Ausführen der delegierten Partition.

Es hat: (Siehe Definition )

  • Init (...): Wird einmal aufgerufen, um eine einmalige Initialisierung durchzuführen.
  • Vorbereiten (...): Wird für jede unterschiedliche Instanz dieses Knotens aufgerufen. Dies geschieht, wenn Sie mehrere delegierte Partitionen haben. Normalerweise möchten Sie hier Speicherzuweisungen vornehmen, da dies jedes Mal aufgerufen wird, wenn die Größe der Tensoren geändert wird.
  • Invoke (...): wird zur Inferenz aufgerufen.

Beispiel

In diesem Beispiel erstellen Sie einen sehr einfachen Delegaten, der nur zwei Arten von Operationen (ADD) und (SUB) mit float32-Tensoren unterstützen kann.

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

Erstellen Sie als Nächstes Ihren eigenen Delegatenkern, indem Sie von SimpleDelegateKernelInterface erben

// 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_;
};


Benchmarking und Bewertung des neuen Delegierten

TFLite verfügt über eine Reihe von Tools, die Sie schnell mit einem TFLite-Modell testen können.

  • Modell-Benchmark-Tool : Das Tool verwendet ein TFLite-Modell, generiert zufällige Eingaben und führt das Modell dann wiederholt für eine bestimmte Anzahl von Läufen aus. Am Ende werden aggregierte Latenzstatistiken gedruckt.
  • Inference Diff Tool : Für ein bestimmtes Modell generiert das Tool zufällige Gaußsche Daten und leitet sie durch zwei verschiedene TFLite-Interpreter, von denen einer einen CPU-Kernel mit einem Thread ausführt und der andere eine benutzerdefinierte Spezifikation verwendet. Es misst die absolute Differenz zwischen den Ausgangstensoren von jedem Interpreter auf Elementbasis. Dieses Tool kann auch beim Debuggen von Genauigkeitsproblemen hilfreich sein.
  • Es gibt auch aufgabenspezifische Bewertungswerkzeuge zur Bildklassifizierung und Objekterkennung. Diese Tools finden Sie hier

Darüber hinaus verfügt TFLite über eine Vielzahl von Kernel- und Op-Unit-Tests, die wiederverwendet werden können, um den neuen Delegaten mit größerer Abdeckung zu testen und sicherzustellen, dass der reguläre TFLite-Ausführungspfad nicht unterbrochen wird.

Um die Wiederverwendung von TFLite-Tests und -Tools für den neuen Delegaten zu erreichen, können Sie eine der beiden folgenden Optionen verwenden:

Den besten Ansatz wählen

Beide Ansätze erfordern einige Änderungen, wie nachstehend beschrieben. Der erste Ansatz verbindet den Delegierten jedoch statisch und erfordert die Neuerstellung der Test-, Benchmarking- und Evaluierungswerkzeuge. Im Gegensatz dazu macht der zweite den Delegaten zu einer gemeinsam genutzten Bibliothek und erfordert, dass Sie die Methoden zum Erstellen / Löschen aus der gemeinsam genutzten Bibliothek verfügbar machen.

Infolgedessen funktioniert der Mechanismus für externe Delegate mit den vorgefertigten Tensorflow Lite-Tool-Binärdateien von TFLite . Die Einrichtung in weniger automatisierten Integrationstests ist jedoch weniger explizit und möglicherweise komplizierter. Verwenden Sie zur besseren Übersicht den Ansatz des Delegiertenregistrars.

Option 1: Nutzen Sie den Delegierten-Registrar

Der Delegatenregistrar führt eine Liste von Delegatenanbietern, von denen jeder eine einfache Möglichkeit bietet, TFLite-Delegaten basierend auf Befehlszeilenflags zu erstellen, und daher für Tools geeignet ist. Stecken in der neuen Delegierten für alle Tensorflow Lite oben genannten Tools erstellen Sie zunächst einen neuen Delegaten Anbieter wie diese ein , und dann macht nur ein paar Änderungen an der BUILD - Regeln. Ein vollständiges Beispiel für diesen Integrationsprozess ist unten dargestellt (und Code finden Sie hier ).

Angenommen, Sie haben einen Delegaten, der die SimpleDelegate-APIs implementiert, und die externen "C" -APIs zum Erstellen / Löschen dieses "Dummy" -Delegierten, wie unten gezeigt:

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

Um das "DummyDelegate" in das Benchmark-Tool und das Inferenz-Tool zu integrieren, definieren Sie einen DelegateProvider wie folgt:

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

Die BUILD-Regeldefinitionen sind wichtig, da Sie sicherstellen müssen, dass die Bibliothek immer verknüpft ist und nicht vom Optimierer gelöscht wird.

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

Fügen Sie nun diese beiden Wrapper-Regeln in Ihre BUILD-Datei ein, um eine Version des Benchmark-Tools und des Inferenz-Tools sowie anderer Evaluierungs-Tools zu erstellen, die mit Ihrem eigenen Delegaten ausgeführt werden können.

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

Sie können diesen Delegatenanbieter auch an TFLite-Kernel-Tests anschließen, wie hier beschrieben.

Option 2: Nutzen Sie externe Delegierte

Bei dieser Alternative erstellen Sie zunächst einen externen Delegatenadapter, external_delegate_adaptor.cc, wie unten gezeigt. Es ist zu beachten, dass dieser Ansatz im Vergleich zu Option 1, wie oben erwähnt, etwas weniger bevorzugt ist.

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

Erstellen Sie nun das entsprechende BUILD-Ziel, um eine dynamische Bibliothek wie unten gezeigt zu erstellen:

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

Nachdem diese .so-Datei für externe Delegaten erstellt wurde, können Sie Binärdateien erstellen oder vorgefertigte Binärdateien verwenden, um sie mit dem neuen Delegaten auszuführen, solange die Binärdatei mit der Bibliothek external_delegate_provider verknüpft ist, die Befehlszeilenflags wie hier beschrieben unterstützt. Hinweis: Dieser externe Delegatenanbieter wurde bereits mit vorhandenen Test- und Tooling-Binärdateien verknüpft.

In den Beschreibungen hier finden Sie eine Illustration zum Benchmarking des Dummy-Delegaten über diesen Ansatz für externe Delegierte. Sie können ähnliche Befehle für die zuvor erwähnten Test- und Bewertungstools verwenden.

Es ist erwähnenswert, dass der externe Delegat die entsprechende C ++ - Implementierung des Delegaten in der Tensorflow Lite Python-Bindung ist, wie hier gezeigt. Daher kann die hier erstellte dynamische externe Delegatenadapterbibliothek direkt mit Tensorflow Lite Python-APIs verwendet werden.

Ressourcen

Betriebssystem BOGEN BINARY_NAME
Linux x86_64
Arm
aarch64
Android Arm
aarch64