Opérateurs personnalisés

Étant donné que la bibliothèque d'opérateurs intégrée TensorFlow Lite ne prend en charge qu'un nombre limité d'opérateurs TensorFlow, tous les modèles ne sont pas convertibles. Pour plus de détails, reportez-vous à Compatibilité des opérateurs .

Pour permettre la conversion, les utilisateurs peuvent fournir leur propre implémentation personnalisée d'un opérateur TensorFlow non pris en charge dans TensorFlow Lite, appelé opérateur personnalisé. Si, à la place, vous souhaitez combiner une série d'opérateurs TensorFlow non pris en charge (ou pris en charge) en un seul opérateur personnalisé optimisé fusionné, reportez-vous à fusion d'opérateurs .

L'utilisation d'opérateurs personnalisés comprend quatre étapes.

Passons en revue un exemple de bout en bout d'exécution d'un modèle avec un opérateur personnalisé tf.atan (nommé Atan , reportez-vous à #create_a_tensorflow_model) qui est pris en charge dans TensorFlow, mais non pris en charge dans TensorFlow Lite.

L'opérateur TensorFlow Text est un exemple d'opérateur personnalisé. Consultez le didacticiel Convertir TF Text en TF Lite pour un exemple de code.

Exemple : opérateur Atan personnalisé

Passons en revue un exemple de prise en charge d'un opérateur TensorFlow que TensorFlow Lite ne possède pas. Supposons que nous utilisons l'opérateur Atan et que nous construisons un modèle très simple pour une fonction y = atan(x + offset) , où offset peut être entraîné.

Créer un modèle TensorFlow

L'extrait de code suivant entraîne un modèle TensorFlow simple. Ce modèle contient simplement un opérateur personnalisé nommé Atan , qui est une fonction y = atan(x + offset) , où offset peut être entraîné.

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

À ce stade, si vous essayez de générer un modèle TensorFlow Lite avec les indicateurs de convertisseur par défaut, vous obtiendrez le message d'erreur suivant :

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

Convertir en un modèle TensorFlow Lite

Créez un modèle TensorFlow Lite avec des opérateurs personnalisés en définissant l'attribut de convertisseur allow_custom_ops comme indiqué ci-dessous :

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

À ce stade, si vous l'exécutez avec l'interpréteur par défaut en utilisant des commandes telles que celles-ci :

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

Vous obtiendrez toujours l'erreur :

Encountered unresolved custom op: Atan.

Créez et enregistrez l'opérateur.

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

Les opérateurs personnalisés TensorFlow Lite sont définis à l'aide d'une simple API en C pur qui consiste en un type opaque ( TfLiteRegistrationExternal ) et des fonctions associées.

TfLiteRegistrationExternal est un type opaque :

typedef struct TfLiteRegistrationExternal TfLiteRegistrationExternal;

TfLiteRegistrationExternal stocke l'identité et l'implémentation de l'opérateur. (Notez que l'opérateur est distinct de ses opérandes, qui sont stockés dans les nœuds du graphique TF Lite pour les nœuds qui appellent l'opérateur.)

Les instances de ce type sont construites avec des appels à TfLiteRegistrationExternalCreate et peuvent être détruites en appelant TfLiteRegistrationExternalDelete .

L'identité de l'opérateur est définie via les paramètres de la fonction constructeur 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.
);

L'implémentation de l'opérateur peut définir des « méthodes » avec les signatures suivantes. Toutes ces méthodes sont facultatives, mais pour qu'un opérateur soit évalué avec succès, l'implémentation de l'opérateur doit définir et définir (à l'aide des fonctions setter) au moins les méthodes Prepare et 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);

Les noms de fonctions (ou préfixes d'espace de noms, pour C++) dans votre implémentation d'opérations ne doivent pas nécessairement correspondre aux noms de fonctions dans l'extrait de code ci-dessus, car l'API d'opérations personnalisées TF Lite n'utilisera que leurs adresses. En effet nous vous recommandons de les déclarer dans un espace de noms anonyme ou sous forme de fonctions statiques.

Mais c'est une bonne idée d'inclure le nom de votre opérateur comme espace de noms ou préfixe sur ces noms de fonctions :

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.
      

Puisqu'il s'agit d'une API C, ces "méthodes" sont implémentées en tant que pointeurs de fonction C dans le type TfLiteRegistrationExternal , qui sont définis en transmettant les adresses de vos fonctions d'implémentation aux fonctions de définition correspondantes TfLiteRegistrationExternalSet MethodName :

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

Reportez-vous à common.h pour plus de détails sur TfLiteContext et TfLiteNode . TfLiteContext fournit des fonctionnalités de rapport d'erreurs et un accès aux objets globaux, y compris tous les tenseurs. TfLiteNode permet aux implémentations d'opérateurs d'accéder à leurs entrées et sorties.

Lorsque l'interpréteur charge un modèle, il appelle la méthode Init() une fois pour chaque nœud du graphique. Un Init() donné sera appelé plus d'une fois si l'opération est utilisée plusieurs fois dans le graphique. Pour les opérations personnalisées, un tampon de configuration sera fourni, contenant un flexbuffer qui mappe les noms de paramètres à leurs valeurs. Le tampon est vide pour les opérations intégrées car l'interpréteur a déjà analysé les paramètres de l'opération. Les implémentations de noyau qui nécessitent un état doivent l'initialiser ici et transférer la propriété à l'appelant. Pour chaque appel Init() , il y aura un appel correspondant à Free() , permettant aux implémentations de disposer du tampon qu'elles auraient pu allouer dans Init() .

Chaque fois que les tenseurs d'entrée sont redimensionnés, l'interpréteur parcourt le graphique pour notifier les implémentations du changement. Cela leur donne la possibilité de redimensionner leur tampon interne, de vérifier la validité des formes et des types d'entrée et de recalculer les formes de sortie. Tout cela se fait via la méthode Prepare() et les implémentations peuvent accéder à leur état en utilisant TfLiteOpaqueNodeGetUserData(node) .

Enfin, chaque fois que l'inférence s'exécute, l'interpréteur parcourt le graphe en appelant la méthode Invoke() , et ici aussi l'état est disponible sous la forme TfLiteOpaqueNodeGetUserData(node) .

Les opérations personnalisées peuvent être implémentées en définissant ces fonctions de « méthode », puis en définissant une fonction qui renvoie une instance de TfLiteRegistrationExternal construite en appelant TfLiteRegistrationExternalCreate puis les méthodes de définition appropriées :

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

Notez que l'enregistrement n'est pas automatique et qu'un appel explicite à votre fonction MyCustomOpRegistration doit être effectué (voir détails ci-dessous). Alors que le BuiltinOpResolver standard (disponible à partir de la cible :builtin_ops ) s'occupe de l'enregistrement des fonctions intégrées, les opérations personnalisées devront être collectées dans des bibliothèques personnalisées distinctes.

Définir le noyau dans le runtime TensorFlow Lite

Tout ce que nous devons faire pour utiliser l'op dans TensorFlow Lite est de définir deux fonctions ( Prepare et Eval ) et une troisième pour construire un 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;
}
      

Lors de l'initialisation de OpResolver , ajoutez l'opération personnalisée dans le résolveur (voir ci-dessous pour un exemple). Cela enregistrera l'opérateur auprès de Tensorflow Lite afin que TensorFlow Lite puisse utiliser la nouvelle implémentation. Notez que les deux derniers arguments de TfLiteRegistration correspondent aux fonctions AtanPrepare et AtanEval que vous avez définies pour l'opération personnalisée. Si vous utilisiez les fonctions AtanInit et AtanFree pour initialiser les variables utilisées dans l'opération et pour libérer de l'espace, respectivement, elles seraient alors ajoutées aux deux premiers arguments de TfLiteRegistration ; ces arguments sont définis sur nullptr dans cet exemple.

Enregistrez l'opérateur auprès de la bibliothèque du noyau

Nous devons maintenant enregistrer l'opérateur auprès de la bibliothèque du noyau. Cela se fait avec un OpResolver . En coulisses, l'interpréteur chargera une bibliothèque de noyaux qui seront affectés à l'exécution de chacun des opérateurs du modèle. Bien que la bibliothèque par défaut ne contienne que des noyaux intégrés, il est possible de la remplacer/l'augmenter par une bibliothèque d'opérateurs personnalisée.

La classe OpResolver , qui traduit les codes et les noms des opérateurs en code réel, est définie comme ceci :

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

Notez que pour des raisons de compatibilité ascendante, cette classe utilise l'ancien type concret TfLiteRegistration plutôt que le type opaque TfLiteRegistrationExternal , mais la structure TfLiteRegistration contient un champ registration_external de type TfLiteRegistrationExternal* .

Les classes MutableOpResolver et BuiltinOpResolver sont dérivées de 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.
};

Une utilisation régulière (sans opérations personnalisées) nécessite que vous utilisiez BuiltinOpResolver et écriviez :

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

Pour ajouter l'opération personnalisée créée ci-dessus, vous pouvez à la place utiliser un MutableOpResolver et appeler AddCustom (avant de transmettre le résolveur au InterpreterBuilder ) :

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

Si l'ensemble des opérations intégrées est jugé trop grand, un nouvel OpResolver pourrait être généré par le code sur la base d'un sous-ensemble d'opérations donné, éventuellement uniquement celles contenues dans un modèle donné. C'est l'équivalent de l'enregistrement sélectif de TensorFlow (et une version simple de celui-ci est disponible dans le répertoire tools ).

Si vous souhaitez définir vos opérateurs personnalisés en Java, vous devrez actuellement créer votre propre couche JNI personnalisée et compiler votre propre AAR dans ce code jni . De même, si vous souhaitez définir ces opérateurs disponibles en Python vous pouvez placer vos inscriptions dans le code wrapper Python .

Notez qu'un processus similaire à celui ci-dessus peut être suivi pour prendre en charge un ensemble d'opérations au lieu d'un seul opérateur. Ajoutez simplement autant d’opérateurs AddCustom que nécessaire. De plus, MutableOpResolver vous permet également de remplacer les implémentations des fonctions intégrées en utilisant AddBuiltin .

Testez et profilez votre opérateur

Pour profiler votre opération avec l'outil de référence TensorFlow Lite, vous pouvez utiliser l' outil de modèle de référence pour TensorFlow Lite. À des fins de test, vous pouvez informer votre version locale de TensorFlow Lite de votre opération personnalisée en ajoutant l'appel AddCustom approprié (comme indiqué ci-dessus) à register.cc.

Les meilleures pratiques

  1. Optimisez les allocations et les désallocations de mémoire avec prudence. L'allocation de mémoire dans Prepare est plus efficace que dans Invoke , et l'allocation de mémoire avant une boucle est meilleure qu'à chaque itération. Utilisez des données tenseurs temporaires plutôt que de vous malallouer (voir point 2). Utilisez des pointeurs/références au lieu de copier autant que possible.

  2. Si une structure de données persiste pendant toute l'opération, nous vous conseillons de pré-allouer la mémoire à l'aide de tenseurs temporaires. Vous devrez peut-être utiliser une structure OpData pour référencer les indices tensoriels dans d'autres fonctions. Voir l'exemple dans le noyau pour la convolution . Un exemple d’extrait de code se trouve ci-dessous.

    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. Si cela ne coûte pas trop de mémoire gaspillée, préférez utiliser un tableau statique de taille fixe (ou un std::vector pré-alloué dans Resize ) plutôt que d'utiliser un std::vector alloué dynamiquement à chaque itération d'exécution.

  4. Évitez d'instancier des modèles de conteneurs de bibliothèque standard qui n'existent pas déjà, car ils affectent la taille binaire. Par exemple, si vous avez besoin d'un std::map dans votre opération qui n'existe pas dans d'autres noyaux, l'utilisation d'un std::vector avec mappage d'indexation direct pourrait fonctionner tout en gardant la taille binaire petite. Découvrez ce que les autres noyaux utilisent pour obtenir un aperçu (ou demander).

  5. Vérifiez le pointeur vers la mémoire renvoyée par malloc . Si ce pointeur est nullptr , aucune opération ne doit être effectuée à l'aide de ce pointeur. Si vous malloc une fonction et que vous rencontrez une erreur de sortie, libérez la mémoire avant de quitter.

  6. Utilisez TF_LITE_OPAQUE_ENSURE(context, condition) pour vérifier une condition spécifique. Votre code ne doit pas laisser la mémoire en suspens lorsque TF_LITE_OPAQUE_ENSURE est utilisé, c'est-à-dire que ces macros doivent être utilisées avant l'allocation de ressources susceptibles de fuir.