Operatory niestandardowe

Wbudowana biblioteka operatora TensorFlow Lite obsługuje tylko ograniczoną liczbę operatorów TensorFlow, dlatego nie każdy model da się konwertować. Więcej informacji znajdziesz w artykule na temat zgodności operatorów.

Aby umożliwić konwersję, użytkownicy mogą udostępnić własną niestandardową implementację nieobsługiwanego operatora TensorFlow w TensorFlow Lite, tzw. operatora niestandardowego. Jeśli chcesz połączyć serię nieobsługiwanych (lub obsługiwanych) operatorów TensorFlow w jeden scalony zoptymalizowany operator niestandardowy, zapoznaj się z informacjami o łączeniu operatorów.

Korzystanie z operatorów niestandardowych składa się z czterech etapów.

Przyjrzyjmy się kompleksowemu przykładowi uruchomienia modelu z operatorem niestandardowych tf.atan (o nazwie Atan). Więcej informacji znajdziesz w sekcji Tworzenie modelu TensorFlow, który jest obsługiwany w TensorFlow, ale nieobsługiwany w TensorFlow Lite.

Przykładem niestandardowego operatora niestandardowego jest operator TensorFlow Text. Przykład kodu znajdziesz w samouczku Konwertowanie tekstu z TF na TF Lite.

Przykład: niestandardowy operator Atan

Omówmy przykład obsługi operatora TensorFlow, którego nie ma TensorFlow Lite. Załóżmy, że używamy operatora Atan i tworzymy bardzo prosty model dla funkcji y = atan(x + offset), w której offset można trenować.

Tworzenie modelu TensorFlow

Poniższy fragment kodu trenuje prosty model TensorFlow. Ten model zawiera po prostu operator niestandardowy o nazwie Atan, który jest funkcją y = atan(x + offset), gdzie offset można trenować.

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

Jeśli w tym momencie spróbujesz wygenerować model TensorFlow Lite z domyślnymi flagami konwertera, otrzymasz następujący komunikat o błędzie:

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

Konwertowanie na model TensorFlow Lite

Utwórz model TensorFlow Lite z niestandardowymi operatorami, ustawiając atrybut konwertera allow_custom_ops, jak pokazano poniżej:

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

Na tym etapie, jeśli uruchomisz go przy użyciu domyślnego tłumaczenia rozmowy za pomocą poleceń takich jak:

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

Nadal będzie pojawiać się błąd:

Encountered unresolved custom op: Atan.

Utwórz i zarejestruj operator.

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

Operatory niestandardowe TensorFlow Lite definiuje się za pomocą prostego interfejsu API czystego C, który składa się z typu nieprzezroczystego (TfLiteRegistrationExternal) i powiązanych funkcji.

TfLiteRegistrationExternal jest typem nieprzezroczystym:

typedef struct TfLiteRegistrationExternal TfLiteRegistrationExternal;

TfLiteRegistrationExternal przechowuje tożsamość i implementację operatora. Pamiętaj, że operator różni się od jego operandów, które są przechowywane w węzłach wykresu TF Lite dla węzłów, które go wywołują.

Instancje tego typu są tworzone przy użyciu wywołań TfLiteRegistrationExternalCreate i można je niszczyć, wywołując metodę TfLiteRegistrationExternalDelete.

Tożsamość operatora jest ustawiana za pomocą parametrów funkcji konstruktora TfLiteRegistrationExternalCreate:

TfLiteRegistrationExternal*
TfLiteRegistrationExternalCreate(
    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.
);

Implementacja operatora może definiować „metody” za pomocą tych podpisów. Wszystkie te metody są opcjonalne, ale aby operator mógł zostać oceniony, implementacja operatora musi zdefiniować i ustawić (za pomocą funkcji ustawiania) co najmniej metody Prepare i Invoke.

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

names funkcji (lub prefiksy przestrzeni nazw w C++) w implementacji operacji nie muszą być takie same jak nazwy funkcji z powyższego fragmentu kodu, ponieważ interfejs TF Lite custom ops API będzie używał tylko ich adresów. Zalecamy zadeklarowanie ich w anonimowej przestrzeni nazw lub jako funkcje statyczne.

Warto jednak podać nazwę operatora jako przestrzeń nazw lub prefiks w nazwach tych funkcji:

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.
      

Ponieważ jest to interfejs API typu C, te „metody” są implementowane jako wskaźniki funkcji C w typie TfLiteRegistrationExternal, które są ustawiane przez przekazanie adresów funkcji implementacji do odpowiednich funkcji ustawiających TfLiteRegistrationExternalSetMethodName:

void TfLiteRegistrationExternalSetInit(
    TfLiteRegistrationExternal* registration,
    void* (*init)(TfLiteOpaqueContext* context, const char* buffer,
                  size_t length));
void TfLiteRegistrationExternalSetFree(
    TfLiteRegistrationExternal* registration,
    void (*free)(TfLiteOpaqueContext* context, void* data));
void TfLiteRegistrationExternalSetPrepare(
    TfLiteRegistrationExternal* registration,
    TfLiteStatus (*prepare)(TfLiteOpaqueContext* context,
                            TfLiteOpaqueNode* node));
void TfLiteRegistrationExternalSetInvoke(
    TfLiteRegistrationExternal* registration,
    TfLiteStatus (*invoke)(TfLiteOpaqueContext* context,
                           TfLiteOpaqueNode* node));
void TfLiteRegistrationExternalSetAsyncKernel(
    TfLiteRegistrationExternal* registration,
    struct TfLiteAsyncKernel* (*async_kernel)(TfLiteOpaqueContext* context,
                                              TfLiteOpaqueNode* node));

Więcej informacji na temat TfLiteContext i TfLiteNode znajdziesz w sekcji common.h. TfLiteContext zapewnia funkcje raportowania błędów i dostęp do obiektów globalnych, w tym wszystkich tensorów. Funkcja TfLiteNode umożliwia implementacjaom operatora dostęp do danych wejściowych i wyjściowych.

Gdy interpreter wczytuje model, wywołuje metodę Init() raz dla każdego węzła na wykresie. Parametr Init() zostanie wywołany więcej niż raz, jeśli operacja zostanie użyta na wykresie więcej niż jeden raz. W przypadku operacji niestandardowych dostępny będzie bufor konfiguracji zawierający bufor elastyczny, który mapuje nazwy parametrów na ich wartości. Bufor jest pusty w przypadku operacji wbudowanych, ponieważ interpreter przeanalizował już parametry operacji. Implementacje jądra, które wymagają stanu, powinny zainicjować go tutaj i przenieść własność na element wywołujący. Każde wywołanie funkcji Init() odpowiada wywołaniu Free(), które umożliwia implementacje pozbyć się bufora, które mogło zostać przydzielone w Init().

Po każdej zmianie rozmiaru tensorów wejściowych tłumacz przechodzi przez wykres, powiadamiając o implementacji zmiany. Dzięki temu mogą zmienić rozmiar wewnętrznego bufora, sprawdzić poprawność kształtów i typów danych wejściowych oraz ponownie obliczyć kształty wyjściowe. Wszystko odbywa się za pomocą metody Prepare(), a implementacje mają dostęp do swojego stanu za pomocą TfLiteOpaqueNodeGetUserData(node).

Na koniec przy każdym uruchomieniu wnioskowania tłumacz przemierzy wykres wywołujący metodę Invoke(). Tutaj też stan jest dostępny jako TfLiteOpaqueNodeGetUserData(node).

Operacje niestandardowe można wdrożyć przez zdefiniowanie tych funkcji „metody”, a następnie zdefiniowanie funkcji, która zwraca wystąpienie obiektu TfLiteRegistrationExternal utworzone przez wywołanie TfLiteRegistrationExternalCreate, a następnie odpowiednie metody seter:

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 TfLiteRegistrationExternal* MyCustomOpRegistrationExternal() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteRegistrationExternal* my_custom_op = ()[] {
        TfLiteRegistrationExternal* r =
            TfLiteRegistrationExternalCreate(
                kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
        TfLiteRegistrationExternalSetInit(r, Init);
        TfLiteRegistrationExternalSetFree(r, Free);
        TfLiteRegistrationExternalSetPrepare(r, Prepare);
        TfLiteRegistrationExternalSetInvoke(r, Eval);
        return r;
      };
    return my_custom_op;
  }

  const TfLiteRegistration* MyCustomOpRegistration() {
    static const TfLiteRegistration my_custom_op {
      .registration_external = MyCustomOpRegistrationExternal();
    };
    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 TfLiteRegistrationExternal* MyCustomOpCreate() {
  const TfLiteRegistrationExternal* r =
      TfLiteRegistrationExternalCreate(
          kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
  TfLiteRegistrationExternalSetInit(r, MyCustomOpInit);
  TfLiteRegistrationExternalSetFree(r, MyCustomOpFree);
  TfLiteRegistrationExternalSetPrepare(r, MyCustomOpPrepare);
  TfLiteRegistrationExternalSetInvoke(r, MyCustomOpEval);
  return r;
}

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

const TfLiteRegistration MyCustomOpRegistration() {
  static const TfLiteRegistration my_custom_op {
    .registration_external = MyCustomOpRegistrationExternal();
  };
  return my_custom_op;
}
      

Pamiętaj, że rejestracja nie jest automatyczna i powinno zostać wykonane jawne wywołanie funkcji MyCustomOpRegistration (szczegóły poniżej). Standardowa BuiltinOpResolver (dostępna w środowisku docelowym :builtin_ops) zajmuje się rejestracją wbudowanych, ale operacje niestandardowe będą musiały być gromadzone w osobnych bibliotekach niestandardowych.

Definiowanie jądra w środowisku wykonawczym TensorFlow Lite

Aby użyć operacji w TensorFlow Lite, musimy zdefiniować 2 funkcje (Prepare i Eval) oraz trzeci, by utworzyć TfLiteRegistrationExternal:

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(TfLiteOpaqueTensorData(input));
      float* output_data = static_cast(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 TfLiteRegistrationExternal* AtanOpRegistrationExternal() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteRegistrationExternal* atan_op = ()[] {
        auto* r = TfLiteRegistrationExternalCreate(
            kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
        TfLiteRegistrationExternalSetPrepare(r, Prepare);
        TfLiteRegistrationExternalSetInvoke(r, Eval);
        return r;
      };
    return atan_op;
  }

  const TfLiteRegistration AtanOpRegistration() {
    static const TfLiteRegistration atan_op {
      .registration_external = AtanOpRegistrationExternal();
    };
    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(TfLiteOpaqueTensorData(input));
  float* output_data = static_cast(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 TfLiteRegistrationExternal* AtanOpCreate() {
  TfLiteRegistrationExternal* r = TfLiteRegistrationExternalCreate(
          kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
  TfLiteRegistrationExternalSetPrepare(r, Prepare);
  TfLiteRegistrationExternalSetInvoke(r, Eval);
  return r;
}

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

const TfLiteRegistration AtanOpRegistration() {
  static const TfLiteRegistration atan_op {
    .registration_external = AtanOpRegistrationExternal();
  };
  return atan_op;
}
      

Podczas inicjowania OpResolver dodaj operację niestandardową w resolverze (przykład znajdziesz poniżej). Spowoduje to zarejestrowanie operatora w Tensorflow Lite, dzięki czemu TensorFlow Lite będzie mógł korzystać z nowej implementacji. Ostatnie 2 argumenty w funkcji TfLiteRegistration odpowiadają funkcjom AtanPrepare i AtanEval zdefiniowanym przez Ciebie na potrzeby operacji niestandardowej. Jeśli użyjesz funkcji AtanInit i AtanFree do zainicjowania zmiennych używanych w operacji i odpowiedniego zwolnienia miejsca na dane, zostaną one dodane do pierwszych dwóch argumentów funkcji TfLiteRegistration. W tym przykładzie te argumenty mają wartość nullptr.

Rejestrowanie operatora w bibliotece jądra

Teraz musimy zarejestrować operatora w bibliotece jądra. Odbywa się to za pomocą OpResolver. W tle interpreter wczyta bibliotekę jąder, która zostanie przypisana do wykonywania wszystkich operatorów w modelu. Domyślna biblioteka zawiera tylko jądra wbudowane, ale można ją zastąpić lub rozszerzyć za pomocą niestandardowych operatorów operacji biblioteki.

Klasa OpResolver, która tłumaczy kody operatorów i nazwy na rzeczywisty kod, jest zdefiniowana w ten sposób:

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

Pamiętaj, że ze względu na zgodność wsteczną ta klasa używa starszego typu betonu (TfLiteRegistration) zamiast nieprzezroczystego typu TfLiteRegistrationExternal, ale struktura TfLiteRegistration zawiera pole registration_external typu TfLiteRegistrationExternal*.

Klasy MutableOpResolver i BuiltinOpResolver pochodzą z OpResolver:

class MutableOpResolver : public OpResolver {
 public:
  MutableOpResolver();  // Constructs an initially empty op resolver.
  void AddBuiltin(tflite::BuiltinOperator op, const TfLiteRegistration* registration) = 0;
  void AddCustom(const char* op, const TfLiteRegistration* registration) = 0;
  void AddAll(const MutableOpResolver& other);
  ...
};

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

Standardowe użycie (bez działań niestandardowych) wymaga użycia BuiltinOpResolver i zapisu:

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

Aby dodać utworzoną powyżej operację niestandardową, możesz użyć polecenia MutableOpResolver i wywołać metodę AddCustom (zanim przekażesz resolver do InterpreterBuilder):

tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
resolver.AddCustom("Atan", AtanOpRegistration());

Jeśli zbiór wbudowanych operacji zostanie uznany za za duży, może zostać wygenerowany nowy kod OpResolver na podstawie podzbioru operacji, prawdopodobnie tylko tych znajdujących się w danym modelu. Jest to odpowiednik selektywnej rejestracji TensorFlow (a jego prosta wersja jest dostępna w katalogu tools).

Jeśli chcesz zdefiniować własne operatory w Javie, musisz obecnie utworzyć własną warstwę JNI i skompilować własny w tym kodzie jni. Jeśli chcesz zdefiniować operatory dostępne w Pythonie, możesz umieścić rejestracje w kodzie otoki Pythona.

Zwróć uwagę, że do obsługi zbioru operacji można zastosować podobną procedurę jak powyżej, a nie pojedynczy operator. Możesz dodać tyle operatorów AddCustom, ile potrzebujesz. Dodatkowo MutableOpResolver umożliwia też zastępowanie implementacji wbudowanych za pomocą AddBuiltin.

Testowanie i profilowanie operatora

Do profilowania operacji za pomocą narzędzia porównawczego TensorFlow Lite możesz użyć narzędzia do modelowania porównawczego dla TensorFlow Lite. Do celów testowych możesz poinformować swoją lokalną kompilację TensorFlow Lite o niestandardowej operacji, dodając odpowiednie wywołanie AddCustom (jak w przykładzie powyżej) do pliku register.cc

Sprawdzone metody

  1. Ostrożnie optymalizuj przydziały pamięci i usuwaj przydziały. Przydzielanie pamięci w komponencie Prepare jest bardziej wydajne niż w Invoke, a przydzielenie pamięci przed pętlą jest lepsze niż w każdej iteracji. Używaj danych z tensorów tymczasowych, zamiast używać samych szyfrów (patrz punkt 2). Używaj wskaźników/odniesień zamiast kopiować, jeśli to możliwe.

  2. Jeśli struktura danych będzie obowiązywać przez cały czas działania, zalecamy wstępne przydzielenie pamięci za pomocą tensorów tymczasowych. Do odwoływania się do indeksów tensorów w innych funkcjach może być konieczne użycie struktury OpData. Zobacz przykład w jądrorze splotu. Poniżej znajduje się przykładowy fragment kodu.

    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. Jeśli nie wiąże się to z nadmiernym zużyciem pamięci, preferuj statyczną tablicę o stałym rozmiarze (lub wstępnie przydzielonego std::vector w Resize), zamiast korzystać z dynamicznie przydzielanego std::vector przy każdym powtórzeniu wykonania.

  4. Unikaj tworzenia instancji standardowych szablonów kontenerów biblioteki, które jeszcze nie istnieją, ponieważ wpływają one na rozmiar plików binarnych. Jeśli na przykład potrzebujesz w operacji obiektu std::map, którego nie ma w innych jądrach, możesz użyć std::vector z mapowaniem indeksowania bezpośredniego, przy zachowaniu niewielkiego rozmiaru pliku binarnego. Zobacz, jak inne jądra wykorzystują inne jądra do zdobywania statystyk (lub zadawania pytań).

  5. Sprawdź wskaźnik do pamięci zwróconej przez malloc. Jeśli ten wskaźnik to nullptr, nie należy za jego pomocą wykonywać żadnych operacji. Jeśli kod malloc w funkcji zostanie zamknięty i wystąpi błąd, przed wyjściem zwolnij miejsce w pamięci.

  6. Aby sprawdzić konkretny warunek, użyj funkcji TF_LITE_OPAQUE_ENSURE(context, condition). Kod nie może pozostawiać w stanie pamięci podczas korzystania z funkcji TF_LITE_OPAQUE_ENSURE, tzn. tych makr należy użyć przed przydzieleniem zasobów, które zostaną ujawnione.