Benutzerdefinierte Operatoren

Da die integrierte LiteRT-Operatorbibliothek nur eine begrenzte Anzahl von TensorFlow-Operatoren unterstützt, kann nicht jedes Modell konvertiert werden. Weitere Informationen finden Sie unter Kompatibilität mit Mobilfunkanbietern.

Damit die Konvertierung möglich ist, können Nutzer ihre eigene benutzerdefinierte Implementierung eines nicht unterstützten TensorFlow-Operators in LiteRT bereitstellen. Dies wird als benutzerdefinierter Operator bezeichnet. Wenn Sie stattdessen eine Reihe von nicht unterstützten (oder unterstützten) TensorFlow-Operatoren in einem einzelnen optimierten benutzerdefinierten Operator zusammenfassen möchten, lesen Sie den Abschnitt Operator-Fusing.

Die Verwendung benutzerdefinierter Operatoren umfasst vier Schritte.

  • TensorFlow-Modell erstellen Achten Sie darauf, dass sich das gespeicherte Modell (oder die Graph-Definition) auf den LiteRT-Operator mit dem richtigen Namen bezieht.

  • In ein LiteRT-Modell konvertieren: Achten Sie darauf, dass Sie das richtige LiteRT-Konverterattribut festlegen, damit das Modell erfolgreich konvertiert werden kann.

  • Operator erstellen und registrieren So weiß die LiteRT-Laufzeit, wie Ihr Operator und Ihre Parameter in Ihrem Diagramm ausführbarem C/C++-Code zugeordnet werden müssen.

  • Operator testen und profilieren Wenn Sie nur Ihren benutzerdefinierten Operator testen möchten, erstellen Sie am besten ein Modell, das nur Ihren benutzerdefinierten Operator enthält, und verwenden Sie das Programm benchmark_model.

Sehen wir uns ein End-to-End-Beispiel für die Ausführung eines Modells mit einem benutzerdefinierten Operator tf.atan (mit dem Namen Atan, siehe TensorFlow-Modell erstellen) an, der in TensorFlow, aber nicht in LiteRT unterstützt wird.

Der TensorFlow Text-Operator ist ein Beispiel für einen benutzerdefinierten Operator. Ein Codebeispiel finden Sie in der Anleitung zum Konvertieren von TF Text in LiteRT.

Beispiel: Benutzerdefinierter Atan-Operator

Sehen wir uns ein Beispiel für die Unterstützung eines TensorFlow-Operators an, der in LiteRT nicht vorhanden ist. Angenommen, wir verwenden den Atan-Operator und erstellen ein sehr einfaches Modell für eine Funktion y = atan(x + offset), wobei offset trainierbar ist.

TensorFlow-Modell erstellen

Im folgenden Code-Snippet wird ein einfaches TensorFlow-Modell trainiert. Dieses Modell enthält nur einen benutzerdefinierten Operator namens Atan, der eine Funktion y = atan(x + offset) ist, wobei offset trainierbar ist.

import tensorflow as tf

# Define training dataset and variables
x = [-8, 0.5, 2, 2.2, 201]
y = [-1.4288993, 0.98279375, 1.2490457, 1.2679114, 1.5658458]
offset = tf.Variable(0.0)

# Define a simple model which just contains a custom operator named `Atan`
@tf.function(input_signature=[tf.TensorSpec.from_tensor(tf.constant(x))])
def atan(x):
  return tf.atan(x + offset, name="Atan")

# Train model
optimizer = tf.optimizers.Adam(0.01)
def train(x, y):
    with tf.GradientTape() as t:
      predicted_y = atan(x)
      loss = tf.reduce_sum(tf.square(predicted_y - y))
    grads = t.gradient(loss, [offset])
    optimizer.apply_gradients(zip(grads, [offset]))

for i in range(1000):
    train(x, y)

print("The actual offset is: 1.0")
print("The predicted offset is:", offset.numpy())
The actual offset is: 1.0
The predicted offset is: 0.99999905

Wenn Sie jetzt versuchen, ein LiteRT-Modell mit den Standard-Converter-Flags zu generieren, erhalten Sie die folgende Fehlermeldung:

Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.

In ein LiteRT-Modell konvertieren

Erstellen Sie ein LiteRT-Modell mit benutzerdefinierten Operatoren, indem Sie das Konverterattribut allow_custom_ops wie unten gezeigt festlegen:

converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan)
converter.allow_custom_ops = True
tflite_model = converter.convert()

Wenn Sie das Skript jetzt mit dem Standardinterpreter ausführen, z. B. mit den folgenden Befehlen:

interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

Sie erhalten weiterhin die Fehlermeldung:

Encountered unresolved custom op: Atan.

Operator erstellen und registrieren

#include "third_party/tensorflow/lite/c/c_api.h"
#include "third_party/tensorflow/lite/c/c_api_opaque.h"

LiteRT-benutzerdefinierte Operatoren werden mit einer einfachen Pure-C-API definiert, die aus einem undurchsichtigen Typ (TfLiteOperator) und zugehörigen Funktionen besteht.

TfLiteOperator ist ein undurchsichtiger Typ:

typedef struct TfLiteOperator TfLiteOperator;

TfLiteOperator speichert die Identität und Implementierung des Betreibers. Der Operator unterscheidet sich von seinen Operanden, die in den LiteRT-Grafikknoten für Knoten gespeichert werden, die den Operator aufrufen.

Instanzen dieses Typs werden mit Aufrufen von TfLiteOperatorCreate erstellt und können durch Aufrufen von TfLiteOperatorDelete zerstört werden.

Die Identität des Operators wird über die Parameter der Konstruktorfunktion TfLiteOperatorCreate festgelegt:

TfLiteOperator*
TfLiteOperatorCreate(
    TfLiteBuiltinOperator builtin_code,  // Normally `TfLiteBuiltinCustom`.
    const char* custom_name,  // The name of the custom op.
    int version  // Normally `1` for the first version of a custom op.
);

In der Operatorimplementierung können „Methoden“ mit den folgenden Signaturen definiert werden. Alle diese Methoden sind optional. Damit ein Operator erfolgreich ausgewertet werden kann, müssen in der Operatorimplementierung jedoch mindestens die Methoden Prepare und Invoke definiert und festgelegt werden (mit den Setter-Funktionen).

// Initializes the op from serialized data.
void* Init(TfLiteOpaqueContext* context, const char* buffer, size_t length);

// Deallocates the op.
// The pointer `buffer` is the data previously returned by an Init invocation.
void Free(TfLiteOpaqueContext* context, void* buffer);

// Called when the inputs that this node depends on have been resized.
TfLiteStatus Prepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node);

// Called when the node is executed. (Should read node inputs and write to
// node outputs).
TfLiteStatus Invoke(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node);

// Retrieves the async kernel.
TfLiteAsyncKernel AsyncKernel(TfLiteOpaqueContext* context,
                              TfLiteOpaqueNode* node);

Die Funktionsnamen names (oder Namespace-Präfixe für C++) in Ihrer Op-Implementierung müssen nicht mit den Funktionsnamen im obigen Code-Snippet übereinstimmen, da die TF Lite Custom Ops API nur ihre Adressen verwendet. Wir empfehlen sogar, sie in einem anonymen Namespace oder als statische Funktionen zu deklarieren.

Es empfiehlt sich jedoch, den Namen des Operators als Namespace oder Präfix in diese Funktionsnamen aufzunehmen:

C++

namespace my_namespace::my_custom_op {
  void* Init(TfLiteOpaqueContext* context,
             const char* buffer, size_t length) { ... }
  // ... plus definitions of Free, Prepare, and Invoke ...
}
      

C

void* MyCustomOpInit(TfLiteOpaqueContext* context,
                     const char* buffer, size_t length) { ... }
// ... plus definitions of MyCustomOpFree, MyCustomOpPrepare, and
// MyCustomOpInvoke.
      

Da es sich um eine C-API handelt, werden diese „Methoden“ als C-Funktionszeiger im Typ TfLiteOperator implementiert. Sie werden festgelegt, indem die Adressen Ihrer Implementierungsfunktionen an die entsprechenden Setter-Funktionen TfLiteOperatorSetMethodName übergeben werden:

void TfLiteOperatorSetInit(
    TfLiteOperator* operator,
    void* (*init)(TfLiteOpaqueContext* context, const char* buffer,
                  size_t length));
void TfLiteOperatorSetFree(
    TfLiteOperator* operator,
    void (*free)(TfLiteOpaqueContext* context, void* data));
void TfLiteOperatorSetPrepare(
    TfLiteOperator* operator,
    TfLiteStatus (*prepare)(TfLiteOpaqueContext* context,
                            TfLiteOpaqueNode* node));
void TfLiteOperatorSetInvoke(
    TfLiteOperator* operator,
    TfLiteStatus (*invoke)(TfLiteOpaqueContext* context,
                           TfLiteOpaqueNode* node));
void TfLiteOperatorSetAsyncKernel(
    TfLiteOperator* operator,
    struct TfLiteAsyncKernel* (*async_kernel)(TfLiteOpaqueContext* context,
                                              TfLiteOpaqueNode* node));

Weitere Informationen zu TfLiteContext und TfLiteNode finden Sie unter common.h. TfLiteContext bietet Funktionen zur Fehlerberichterstattung und Zugriff auf globale Objekte, einschließlich aller Tensoren. Mit TfLiteNode können Operatorimplementierungen auf ihre Ein- und Ausgaben zugreifen.

Wenn der Interpreter ein Modell lädt, ruft er die Methode Init() einmal für jeden Knoten im Diagramm auf. Ein bestimmtes Init() wird mehrmals aufgerufen, wenn der Vorgang mehrmals im Diagramm verwendet wird. Für benutzerdefinierte Vorgänge wird ein Konfigurationspuffer mit einem Flexbuffer bereitgestellt, der Parameternamen ihren Werten zuordnet. Der Puffer ist für integrierte Vorgänge leer, da der Interpreter die Vorgangsparameter bereits geparst hat. Kernelimplementierungen, die einen Status erfordern, sollten ihn hier initialisieren und die Inhaberschaft an den Aufrufer übertragen. Für jeden Init()-Aufruf gibt es einen entsprechenden Aufruf von Free(), sodass Implementierungen den Puffer freigeben können, den sie möglicherweise in Init() zugewiesen haben.

Immer wenn die Größe der Eingabetensoren geändert wird, durchläuft der Interpreter den Graphen und benachrichtigt die Implementierungen über die Änderung. So haben sie die Möglichkeit, ihren internen Puffer anzupassen, die Gültigkeit von Eingabeformen und -typen zu prüfen und Ausgabefomen neu zu berechnen. Dies geschieht alles über die Methode Prepare(). Implementierungen können mit TfLiteOpaqueNodeGetUserData(node) auf ihren Status zugreifen.

Bei jeder Ausführung der Inferenz durchläuft der Interpreter den Graphen und ruft die Methode Invoke() auf. Auch hier ist der Status als TfLiteOpaqueNodeGetUserData(node) verfügbar.

Benutzerdefinierte Vorgänge können implementiert werden, indem diese „method“-Funktionen definiert werden und dann eine Funktion definiert wird, die eine Instanz von TfLiteOperator zurückgibt, die durch Aufrufen von TfLiteOperatorCreate und dann der relevanten Setter-Methoden erstellt wird:

C++

namespace my_namespace::my_custom_op {
  namespace {
    void* Init(TfLiteOpaqueContext* context,
               const char* buffer, size_t length) { ... }
    void Free(TfLiteOpaqueContext* context, void* buffer) { ... }
    TfLiteStatus Prepare(TfLiteOpaqueContext* context,
                         TfLiteOpaqueNode* node) { ... }
    TfLiteStatus Invoke(TfLiteOpaqueContext* context,
                        TfLiteOpaqueNode* node) {... }
  };

  const TfLiteOperator* MyCustomOperator() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteOperator* my_custom_op = ()[] {
        TfLiteOperator* r =
            TfLiteOperatorCreate(
                kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
        TfLiteOperatorSetInit(r, Init);
        TfLiteOperatorSetFree(r, Free);
        TfLiteOperatorSetPrepare(r, Prepare);
        TfLiteOperatorSetInvoke(r, Eval);
        return r;
      };
    return my_custom_op;
  }
}  // namespace my_namespace
      

C

static void* MyCustomOpInit(TfLiteOpaqueContext* context, const char* buffer,
                     size_t length) { ... }
static void MyCustomOpFree(TfLiteOpaqueContext* context, void* buffer) { ... }
static TfLiteStatus MyCustomOpPrepare(TfLiteOpaqueContext* context,
                                      TfLiteOpaqueNode* node) { ... }
static TfLiteStatus MyCustomOpInvoke(TfLiteOpaqueContext* context,
                                     TfLiteOpaqueNode* node) {... }

static TfLiteOperator* MyCustomOpCreate() {
  const TfLiteOperator* r =
      TfLiteOperatorCreate(
          kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
  TfLiteOperatorSetInit(r, MyCustomOpInit);
  TfLiteOperatorSetFree(r, MyCustomOpFree);
  TfLiteOperatorSetPrepare(r, MyCustomOpPrepare);
  TfLiteOperatorSetInvoke(r, MyCustomOpEval);
  return r;
}

const TfLiteOperator* MyCustomOperator() {
  // Singleton instance, intentionally never destroyed.
  static const TfLiteOperator* my_custom_op = MyCustomOpCreate();
  return my_custom_op;
}
      

Die Registrierung erfolgt nicht automatisch. Es muss ein expliziter Aufruf Ihrer MyCustomOperator-Funktion erfolgen (siehe Details unten). Während sich die Standard-BuiltinOpResolver (verfügbar über das :builtin_ops-Ziel) um die Registrierung von Built-ins kümmert, müssen benutzerdefinierte Vorgänge in separaten benutzerdefinierten Bibliotheken gesammelt werden.

Kernel in der LiteRT-Laufzeit definieren

Um den Vorgang in LiteRT zu verwenden, müssen wir nur zwei Funktionen (Prepare und Eval) und eine dritte zum Erstellen eines TfLiteOperator definieren:

C++

namespace atan_op {
  namespace {
    TfLiteStatus AtanPrepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
      TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumInputs(node), 1);
      TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumOutputs(node), 1);

      const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
      TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

      int num_dims = TfLiteOpaqueTensorNumDimensions(input);

      TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
      for (int i=0; i < num_dims; ++i) {
        output_size->data[i] = input->dims->data[i];
      }

      return TfLiteOpaqueContextResizeTensor(context, output, output_size);
    }

    TfLiteStatus AtanEval(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
      const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
      TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

      float* input_data = static_cast<float*>(TfLiteOpaqueTensorData(input));
      float* output_data = static_cast<float*>(TfLiteOpaqueTensorData(output));

      size_t count = 1;
      int num_dims = TfLiteOpaqueTensorNumDimensions(input);
      for (int i = 0; i < num_dims; ++i) {
        count *= input->dims->data[i];
      }

      for (size_t i = 0; i < count; ++i) {
        output_data[i] = atan(input_data[i]);
      }
      return kTfLiteOk;
    }
  }  // anonymous namespace

  const TfLiteOperator* AtanOperator() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteOperator* atan_op = ()[] {
        auto* r = TfLiteOperatorCreate(
            kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
        TfLiteOperatorSetPrepare(r, Prepare);
        TfLiteOperatorSetInvoke(r, Eval);
        return r;
      };
    return atan_op;
  }
}  // namespace atan_op
      

C

static TfLiteStatus AtanPrepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
  TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumInputs(node), 1);
  TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumOutputs(node), 1);

  const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
  TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

  int num_dims = TfLiteOpaqueTensorNumDimensions(input);

  TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
  for (int i = 0; i < num_dims; ++i) {
    output_size->data[i] = input->dims->data[i];
  }

  return TfLiteOpaqueContextResizeTensor(context, output, output_size);
}

static TfLiteStatus AtanEval(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
  const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
  TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

  float* input_data = static_cast<float*>(TfLiteOpaqueTensorData(input));
  float* output_data = static_cast<float*>(TfLiteOpaqueTensorData(output));

  size_t count = 1;
  int num_dims = TfLiteOpaqueTensorNumDimensions(input);
  for (int i = 0; i < num_dims; ++i) {
    count *= input->dims->data[i];
  }

  for (size_t i = 0; i < count; ++i) {
    output_data[i] = atan(input_data[i]);
  }
  return kTfLiteOk;
}

static const TfLiteOperator* AtanOpCreate() {
  TfLiteOperator* r = TfLiteOperatorCreate(
          kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
  TfLiteOperatorSetPrepare(r, Prepare);
  TfLiteOperatorSetInvoke(r, Eval);
  return r;
}

const TfLiteOperator* AtanOperator() {
  // Singleton instance, intentionally never destroyed.
  static const TfLiteOperator* atan_op = AtanOpCreate();
  return atan_op;
}
      

Fügen Sie beim Initialisieren von OpResolver den benutzerdefinierten Vorgang in den Resolver ein (siehe Beispiel unten). Dadurch wird der Operator bei LiteRT registriert, sodass LiteRT die neue Implementierung verwenden kann.

Operator in der Kernel-Bibliothek registrieren

Jetzt müssen wir den Operator in der Kernel-Bibliothek registrieren. Dazu wird eine OpResolver verwendet. Im Hintergrund lädt der Interpreter eine Bibliothek mit Kernels, die für die Ausführung der einzelnen Operatoren im Modell zugewiesen werden. Die Standardbibliothek enthält nur integrierte Kerne. Sie kann jedoch durch eine benutzerdefinierte Bibliothek mit Operatoren ersetzt oder erweitert werden.

Die Klasse OpResolver, die Operatorcodes und ‑namen in tatsächlichen Code übersetzt, ist so definiert:

class OpResolver {
 public:
  virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
  virtual TfLiteRegistration* FindOp(const char* op) const = 0;
  ...
};

Aus Gründen der Abwärtskompatibilität wird in dieser Klasse der ältere konkrete Typ TfLiteRegistration anstelle des undurchsichtigen Typs TfLiteOperator verwendet. Die TfLiteRegistration-Struktur enthält jedoch ein registration_external-Feld vom Typ TfLiteOperator*.

Die Klassen MutableOpResolver und BuiltinOpResolver werden von OpResolver abgeleitet:

class MutableOpResolver : public OpResolver {
 public:
  MutableOpResolver();  // Constructs an initially empty op resolver.
  void AddAll(const MutableOpResolver& other);
  ...
};

class BuiltinOpResolver : public MutableOpResolver {
 public:
  BuiltinOpResolver();  // Constructs an op resolver with all the builtin ops.
};

Für die reguläre Verwendung (ohne benutzerdefinierte Vorgänge) müssen Sie BuiltinOpResolver verwenden und Folgendes schreiben:

tflite::ops::builtin::BuiltinOpResolver resolver;

Um den oben erstellten benutzerdefinierten Vorgang hinzuzufügen, können Sie stattdessen ein MutableOpResolver verwenden und tflite::AddOp aufrufen (bevor Sie den Resolver an InterpreterBuilder übergeben):

tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
tflite::AddOp(&resolver, AtanOpRegistration());

Wenn die Menge der integrierten Vorgänge als zu groß erachtet wird, kann ein neues OpResolver basierend auf einer bestimmten Teilmenge von Vorgängen, möglicherweise nur den in einem bestimmten Modell enthaltenen, code-generiert werden. Dies entspricht der selektiven Registrierung von TensorFlow (eine einfache Version davon ist im Verzeichnis tools verfügbar).

Wenn Sie Ihre benutzerdefinierten Operatoren in Java definieren möchten, müssen Sie derzeit eine eigene benutzerdefinierte JNI-Ebene erstellen und Ihr eigenes AAR in diesem JNI-Code kompilieren. Wenn Sie diese in Python verfügbaren Operatoren definieren möchten, können Sie Ihre Registrierungen im Python-Wrapper-Code platzieren.

Ein ähnlicher Prozess wie oben kann auch für die Unterstützung einer Reihe von Vorgängen anstelle eines einzelnen Operators verwendet werden. Fügen Sie einfach so viele AddCustom-Operatoren hinzu, wie Sie benötigen. Außerdem können Sie mit MutableOpResolver Implementierungen von Built-in-Funktionen mit AddBuiltin überschreiben.

Operator testen und profilieren

Wenn Sie Ihren Vorgang mit dem LiteRT-Benchmark-Tool profilieren möchten, können Sie das Benchmark-Modell-Tool für LiteRT verwenden. Zu Testzwecken können Sie Ihren lokalen Build von LiteRT über Ihren benutzerdefinierten Vorgang informieren, indem Sie den entsprechenden AddCustom-Aufruf (wie oben gezeigt) zu register.cc hinzufügen.

Best Practices

  1. Arbeitsspeicherzuweisungen und ‑freigaben vorsichtig optimieren. Die Zuweisung von Arbeitsspeicher in Prepare ist effizienter als in Invoke. Außerdem ist es besser, Arbeitsspeicher vor einer Schleife zuzuweisen als in jeder Iteration. Verwenden Sie temporäre Tensordaten anstatt selbst Speicher zuzuweisen (siehe Punkt 2). Verwenden Sie nach Möglichkeit Zeiger/Referenzen anstelle von Kopien.

  2. Wenn eine Datenstruktur während des gesamten Vorgangs bestehen bleibt, empfehlen wir, den Speicher mit temporären Tensoren vorab zuzuweisen. Möglicherweise müssen Sie eine OpData-Struktur verwenden, um in anderen Funktionen auf die Tensor-Indexe zu verweisen. Beispiel für den Kernel für die Faltung Unten finden Sie ein Beispielcode-Snippet.

    struct MyOpData {
      int temp_tensor_index;
      ...
    };
    
    void* Init(TfLiteOpaqueContext* context,
        const char* buffer, size_t length) {
      auto* op_data = new MyOpData{};
      ...
      return op_data;
    }
    void Free(TfLiteOpaqueContext* context, void* buffer) {
      ...
      delete reinterpret_cast<MyOpData*>(buffer);
    }
    TfLiteStatus Prepare(TfLiteOpaqueContext* context,
                         TfLiteOpaqueNode* node) {
      ...
      auto* op_data =
          reinterpret_cast<MyOpData*>(TfLiteOpaqueNodeGetUserData(node));
      const int num_temporaries = 1;
      int temporary_tensor_indices[num_temporaries];
      TfLiteOpaqueTensorBuilder* builder = TfLiteOpaqueTensorBuilderCreate();
      TfLiteOpaqueTensorBuilderSetType(builder, kTfLiteFloat32);
      TfLiteOpaqueTensorBuilderSetAllocationType(builder, kTfLiteArenaRw);
      TfLiteOpaqueContextAddTensor(context, builder,
          &temporary_tensor_indices[0]);
      TfLiteOpaqueTensorBuilderDelete(builder);
      TfLiteOpaqueNodeSetTemporaries(node, temporary_tensor_indices,
          num_temporaries);
      op_data->temp_tensor_index = temporary_tensor_indices[0];
      ...
      return kTfLiteOk;
    }
    TfLiteStatus Invoke(TfLiteOpaqueContext* context,
                        TfLiteOpaqueNode* node) {
      ...
      auto* op_data = reinterpret_cast<MyOpData*>(
          TfLiteOpaqueNodeGetUserData(node));
      TfLiteOpaqueTensor* temp_tensor =
          TfLiteOpaqueContextGetOpaqueTensor(context,
              op_data->temp_tensor_index);
      TF_LITE_OPAQUE_ENSURE(context,
          TfLiteTensorType(temp_tensor) == kTfLiteFloat32);
      TF_LITE_OPAQUE_ENSURE(context,
          TfLiteTensorGetAllocationType(temp_Tensor) == kTfLiteArenaRw);
      void *temp_data = TfLiteTensorData(temp_tensor);
      TF_LITE_OPAQUE_ENSURE(context, temp_data != nullptr);
      ...
      return kTfLiteOk;
    }
    
  3. Wenn nicht zu viel Speicher verschwendet wird, sollten Sie ein statisches Array mit fester Größe (oder ein vorab zugewiesenes std::vector in Resize) verwenden, anstatt bei jeder Ausführung ein dynamisch zugewiesenes std::vector zu verwenden.

  4. Vermeiden Sie das Instanziieren von Standardbibliotheks-Container-Vorlagen, die noch nicht vorhanden sind, da sie sich auf die Binärgröße auswirken. Wenn Sie beispielsweise eine std::map in Ihrem Vorgang benötigen, die in anderen Kernels nicht vorhanden ist, kann die Verwendung einer std::vector mit direkter Indexierungszuordnung funktionieren und gleichzeitig die Binärgröße gering halten. Sehen Sie sich an, was andere Kernels verwenden, um Erkenntnisse zu gewinnen (oder fragen Sie danach).

  5. Prüfen Sie den von malloc zurückgegebenen Zeiger auf den Speicher. Wenn dieser Zeiger nullptr ist, sollten keine Vorgänge mit diesem Zeiger ausgeführt werden. Wenn Sie malloc in einer Funktion verwenden und einen Fehlerbeendigungszweig haben, geben Sie den Speicher frei, bevor Sie die Funktion beenden.

  6. Verwenden Sie TF_LITE_OPAQUE_ENSURE(context, condition), um einen bestimmten Zustand zu prüfen. Ihr Code darf keinen Speicher zurücklassen, wenn TF_LITE_OPAQUE_ENSURE verwendet wird. Diese Makros sollten also verwendet werden, bevor Ressourcen zugewiesen werden, die verloren gehen.