Benutzerdefinierte Delegierung implementieren

Mit einem TensorFlow Lite-Delegaten können Sie Ihre Modelle (teilweise oder ganz) auf einem anderen Executor ausführen. Dieser Mechanismus kann eine Vielzahl von On-Device-Beschleunigern wie die GPU oder Edge TPU (Tensor Processing Unit) für die Inferenz nutzen. Dies bietet Entwicklern eine flexible und entkoppelte Methode vom Standard-TFLite zur Beschleunigung der Inferenz.

Im Diagramm unten sind die Bevollmächtigten zusammengefasst. Weitere Informationen finden Sie in den Abschnitten weiter unten.

TFLite-Delegierungen

Wann sollte ich einen benutzerdefinierten Delegaten erstellen?

TensorFlow Lite hat eine Vielzahl von Bevollmächtigten für Zielbeschleuniger wie GPU, DSP und EdgeTPU.

Das Erstellen eines eigenen Bevollmächtigten ist in den folgenden Szenarien nützlich:

  • Sie möchten eine neue ML-Inferenz-Engine einbinden, die von keinem vorhandenen Bevollmächtigten unterstützt wird.
  • Sie haben einen benutzerdefinierten Hardwarebeschleuniger, der die Laufzeit bei bekannten Szenarien verbessert.
  • Sie entwickeln CPU-Optimierungen (z. B. Operatorfusion), die bestimmte Modelle beschleunigen können.

Wie funktionieren Bevollmächtigte?

Betrachten Sie eine einfache Modellgrafik wie die folgende und einen Delegaten „MyDelegate“, der eine schnellere Implementierung für Conv2D- und Mittelwertvorgänge bietet.

Ursprüngliche Grafik

Nach der Anwendung von „MyDelegate“ wird die ursprüngliche TensorFlow Lite-Grafik so aktualisiert:

Mit Bevollmächtigtem grafisch darstellen

Die obige Grafik ergibt sich, weil TensorFlow Lite die ursprüngliche Grafik nach zwei Regeln teilt:

  • Bestimmte Vorgänge, die vom Bevollmächtigten verarbeitet werden könnten, werden in eine Partition eingefügt, während gleichzeitig die ursprünglichen Abhängigkeiten zwischen dem Computing-Workflow zwischen den Vorgängen berücksichtigt werden.
  • Jede zu delegierende Partition hat nur Eingabe- und Ausgabeknoten, die nicht vom Bevollmächtigten verwaltet werden.

Jede von einem Bevollmächtigten verarbeitete Partition wird in der ursprünglichen Grafik durch einen Delegaten Knoten ersetzt, der die Partition bei ihrem Aufrufaufruf auswertet. Dieser kann auch als Delegate-Kernel aufgerufen werden.

Je nach Modell kann der endgültige Graph am Ende einen oder mehrere Knoten haben, was bedeutet, dass einige Vorgänge vom Bevollmächtigten nicht unterstützt werden. Im Allgemeinen sollten vom Bevollmächtigten nicht mehrere Partitionen verwaltet werden, da bei jedem Wechsel vom Bevollmächtigten zur Hauptgrafik ein Mehraufwand für die Übergabe der Ergebnisse vom delegierten Teildiagramm an die Hauptgrafik entsteht, die aufgrund von Speicherkopien (z. B. von GPU zur CPU) entsteht. Ein solcher Aufwand kann Leistungssteigerungen aufheben, insbesondere wenn eine große Anzahl von Arbeitsspeicherkopien vorhanden ist.

Eigenen benutzerdefinierten Delegaten implementieren

In der Regel ist es am besten, einen Bevollmächtigten mit der SimpleDelegate API hinzuzufügen.

Zum Erstellen eines neuen Delegaten müssen Sie zwei Schnittstellen implementieren und Ihre eigene Implementierung für die Schnittstellenmethoden bereitstellen.

1–SimpleDelegateInterface

Diese Klasse stellt die Funktionen des Delegaten, welche Vorgänge unterstützt werden, und eine Factory-Klasse zum Erstellen eines Kernels dar, der den delegierten Graphen kapselt. Weitere Informationen finden Sie über die Schnittstelle, die in dieser C++-Headerdatei definiert ist. In den Kommentaren im Code werden die einzelnen APIs im Detail erläutert.

2–SimpleDelegateKernelInterface

Diese Klasse kapselt die Logik für die Initialisierung / Vorbereitung / und Ausführung der delegierten Partition.

Sie enthält: (siehe Definition)

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

Beispiel

In diesem Beispiel erstellen Sie einen sehr einfachen Delegaten, der nur 2 Arten von Vorgängen (ADD) und (SUB) mit float32-Tensoren unterstützt.

// 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 delegierten Kernel durch Übernahme von 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_;
};


Benchmarking und Bewertung des neuen Bevollmächtigten

TFLite bietet 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 Durchläufen aus. Am Ende werden aggregierte Latenzstatistiken ausgegeben.
  • Inference Diff Tool (Inferenzdifferenz-Tool): Für ein bestimmtes Modell generiert das Tool zufällige Gaußsche Daten und übergibt sie zwei verschiedene TFLite-Interpreter, von denen einer mit einem einzelnen Thread-CPU-Kernel ausgeführt wird und der andere eine benutzerdefinierte Spezifikation verwendet. Es misst die absolute Differenz zwischen den Ausgabetensoren von jedem Interpreter auf Elementbasis. Dieses Tool kann auch beim Beheben von Genauigkeitsproblemen hilfreich sein.
  • Es gibt auch aufgabenspezifische Bewertungstools für die Bildklassifizierung und Objekterkennung. Diese Tools finden Sie hier.

Darüber hinaus verfügt TFLite über eine große Reihe von Kernel- und Betriebseinheitstests, die wiederverwendet werden können, um den neuen Delegaten mit mehr Abdeckung zu testen und sicherzustellen, dass der reguläre TFLite-Ausführungspfad nicht unterbrochen wird.

Wenn Sie TFLite-Tests und -Tools für den neuen Bevollmächtigten wiederverwenden möchten, können Sie eine der folgenden beiden Optionen verwenden:

Den besten Ansatz auswählen

Für beide Ansätze sind einige Änderungen erforderlich, wie unten beschrieben. Der erste Ansatz verknüpft den Bevollmächtigten jedoch statisch und erfordert eine Neuerstellung der Test-, Benchmarking- und Evaluationstools. Im Gegensatz dazu wird der Bevollmächtigte mit der zweiten zu einer gemeinsam genutzten Bibliothek. Hier müssen Sie die Methoden zum Erstellen und Löschen aus der gemeinsam genutzten Bibliothek verfügbar machen.

Infolgedessen funktioniert der externe Delegatmechanismus mit den vorgefertigten Tensorflow Lite-Toolbinärdateien von TFLite. Es ist jedoch weniger konkret und die Einrichtung in automatisierten Integrationstests könnte komplizierter sein. Für mehr Klarheit empfiehlt sich der Ansatz mit delegierten Registraren.

Option 1: Bevollmächtigten Registrar nutzen

Der bevollmächtigte Registrar führt eine Liste der delegierten Anbieter, von denen jeder eine einfache Möglichkeit bietet, TFLite-Delegierungen basierend auf Befehlszeilen-Flags zu erstellen, und somit bequem für Tools geeignet sind. Um den neuen Delegaten für alle oben erwähnten Tensorflow Lite-Tools einzurichten, erstellen Sie zuerst einen neuen Delegaten und nehmen dann nur wenige Änderungen an den build-Regeln vor. Ein vollständiges Beispiel für diesen Integrationsprozess wird unten gezeigt (den Code findest du hier).

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

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

Definieren Sie wie unten einen DelegateProvider, um „DummyDelegate“ in das Benchmark-Tool und das Inference-Tool zu integrieren:

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 dafür sorgen müssen, dass die Bibliothek immer verknüpft ist und nicht durch die Optimierung 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, des Inferenztools und anderer Evaluationstools zu erstellen, die mit Ihrem eigenen Bevollmächtigten ausgeführt werden kann.

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 delegierten Anbieter auch für TFLite-Kernel-Tests einbinden, wie hier beschrieben.

Option 2: Externen Bevollmächtigten nutzen

In dieser Alternative erstellen Sie zuerst einen externen Delegaten Adapter, den external_delegate_adaptor.cc, wie unten gezeigt. Hinweis: Dieser Ansatz ist im Vergleich zu Option 1, wie bereits erwähnt, etwas weniger bevorzugt.

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 zu erstellen, wie unten gezeigt:

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 die .so-Datei des externen Delegaten erstellt wurde, können Sie Binärdateien erstellen oder vordefinierte Binärdateien verwenden, die mit dem neuen Delegaten ausgeführt werden. Voraussetzung dafür ist, dass die Binärdatei mit der Bibliothek external_delegate_provider verknüpft ist, die Befehlszeilen-Flags unterstützt, wie hier beschrieben. Hinweis: Dieser externe Delegatanbieter ist bereits mit vorhandenen Binärprogrammen für Test- und Tooltools verknüpft.

In den hier beschriebenen Beschreibungen finden Sie eine Darstellung, wie Sie den Dummy-Delegaten mit diesem Ansatz für externe Bevollmächtigte vergleichen können. Sie können ähnliche Befehle für die zuvor erwähnten Test- und Bewertungstools verwenden.

Beachten Sie, dass der externe Delegate die entsprechende C++-Implementierung des delegates in der TensorFlow Lite-Python-Bindung ist, wie hier gezeigt. Daher kann die hier erstellte dynamische External Delegate-Adapterbibliothek direkt mit den Python APIs von TensorFlow Lite verwendet werden.

Ressourcen

Betriebssystem ARCH BINARY_NAME
Linux x86_64
Verzweigung
aarch64
Android Verzweigung
aarch64