Opérateurs personnalisés

Étant donné que la bibliothèque d'opérateurs intégrée LiteRT n'est compatible qu'avec un nombre limité d'opérateurs TensorFlow, tous les modèles ne sont pas convertibles. Pour en savoir plus, consultez la page Compatibilité avec les opérateurs.

Pour autoriser la conversion, les utilisateurs peuvent fournir leur propre implémentation personnalisée d'un opérateur TensorFlow non compatible dans LiteRT, appelé opérateur personnalisé. Si vous souhaitez plutôt combiner une série d'opérateurs TensorFlow non compatibles (ou compatibles) en un seul opérateur personnalisé optimisé et fusionné, consultez la section 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, voir Créer un modèle TensorFlow) qui est compatible avec TensorFlow, mais pas avec LiteRT.

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

Exemple : opérateur Atan personnalisé

Prenons l'exemple d'un opérateur TensorFlow qui n'est pas disponible dans LiteRT. Supposons que nous utilisions l'opérateur Atan et que nous construisions un modèle très simple pour une fonction y = atan(x + offset), où offset est entraînable.

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 est entraînable.

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 LiteRT avec les indicateurs de convertisseur par défaut, le message d'erreur suivant s'affiche :

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

Convertir en modèle LiteRT

Créez un modèle LiteRT 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 à l'aide de commandes telles que les suivantes :

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

L'erreur suivante s'affiche toujours :

Encountered unresolved custom op: Atan.

Créez et enregistrez l'opérateur.

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

Les opérateurs personnalisés LiteRT sont définis à l'aide d'une API C pure et simple qui se compose d'un type opaque (TfLiteOperator) et de fonctions associées.

TfLiteOperator est un type opaque :

typedef struct TfLiteOperator TfLiteOperator;

TfLiteOperator 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 LiteRT pour les nœuds qui appellent l'opérateur.)

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

L'identité de l'opérateur est définie via les paramètres de la fonction de constructeur TfLiteOperatorCreate :

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

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 fonction (ou les préfixes d'espace de noms, pour C++) dans votre implémentation d'op n'ont pas besoin de correspondre aux noms de fonction dans l'extrait de code ci-dessus, car l'API d'op personnalisés TF Lite n'utilisera que leurs adresses. Nous vous recommandons en effet de les déclarer dans un espace de noms anonyme ou en tant que fonctions statiques.

Toutefois, il est recommandé d'inclure le nom de votre opérateur en tant qu'espace de noms ou préfixe dans les noms de ces 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.
      

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

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

Pour en savoir plus sur TfLiteContext et TfLiteNode, consultez common.h. TfLiteContext fournit des fonctionnalités de signalement des erreurs et un accès aux objets globaux, y compris à tous les Tensors. 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é plusieurs 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. Il contient 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 kernel 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 Free() correspondant, ce qui permettra aux implémentations de se débarrasser du tampon qu'elles ont pu allouer dans Init().

Chaque fois que les Tensors d'entrée sont redimensionnés, l'interpréteur parcourt le graphique et informe les implémentations de la modification. Cela leur permet 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 par le biais de la méthode Prepare(), et les implémentations peuvent accéder à leur état à l'aide de TfLiteOpaqueNodeGetUserData(node).

Enfin, chaque fois que l'inférence s'exécute, l'interpréteur parcourt le graphique en appelant la méthode Invoke(). 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 "méthode", puis en définissant une fonction qui renvoie une instance de TfLiteOperator construite en appelant TfLiteOperatorCreate, puis les méthodes setter pertinentes :

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

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

Définir le kernel dans l'environnement d'exécution LiteRT

Pour utiliser l'opération dans LiteRT, il suffit de définir deux fonctions (Prepare et Eval) et une troisième pour construire un TfLiteOperator :

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

Lors de l'initialisation de OpResolver, ajoutez l'opération personnalisée au résolveur (voir l'exemple ci-dessous). Cela enregistrera l'opérateur auprès de LiteRT afin que LiteRT puisse utiliser la nouvelle implémentation.

Enregistrer 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. Pour ce faire, utilisez un OpResolver. En arrière-plan, l'interpréteur chargera une bibliothèque de noyaux qui seront chargés d'exécuter 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 avec une bibliothèque personnalisée d'opérateurs.

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

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

Notez que, pour assurer la rétrocompatibilité, cette classe utilise l'ancien type concret TfLiteRegistration plutôt que le type opaque TfLiteOperator, mais que la structure TfLiteRegistration contient un champ registration_external de type TfLiteOperator*.

Les classes MutableOpResolver et BuiltinOpResolver sont dérivées de OpResolver :

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

L'utilisation régulière (sans opérations personnalisées) nécessite l'utilisation de BuiltinOpResolver et l'écriture de :

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

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

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

Si l'ensemble des opérations intégrées est jugé trop important, un nouveau OpResolver peut être généré par code en fonction d'un sous-ensemble d'opérations donné, éventuellement uniquement celles contenues dans un modèle donné. Cela équivaut à l'enregistrement sélectif de TensorFlow (une version simple est disponible dans le répertoire tools).

Si vous souhaitez définir vos opérateurs personnalisés en Java, vous devez 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 enregistrements 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. Il vous suffit d'ajouter autant d'opérateurs AddCustom que nécessaire. De plus, MutableOpResolver vous permet également de remplacer les implémentations des intégrations à l'aide de AddBuiltin.

Tester et profiler votre opérateur

Pour profiler votre opération avec l'outil de benchmark LiteRT, vous pouvez utiliser l'outil de modèle de benchmark pour LiteRT. Pour les tests, vous pouvez rendre votre build local de LiteRT compatible avec votre opération personnalisée en ajoutant l'appel AddCustom approprié (comme indiqué ci-dessus) à register.cc.

Bonnes pratiques

  1. Optimisez les allocations et les libérations de mémoire avec précaution. L'allocation de mémoire dans Prepare est plus efficace que dans Invoke, et l'allocation de mémoire avant une boucle est préférable à l'allocation dans chaque itération. Utilisez des données de tenseurs temporaires plutôt que de les allouer vous-même (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 Tensors temporaires. Vous devrez peut-être utiliser une structure OpData pour faire référence aux index de Tensor dans d'autres fonctions. Consultez l'exemple dans le noyau de convolution. Vous trouverez ci-dessous un exemple d'extrait de code.

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

  4. Évitez d'instancier des modèles de conteneur de bibliothèque standard qui n'existent pas déjà, car ils affectent la taille du 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 un mappage d'indexation directe peut fonctionner tout en conservant une petite taille binaire. Consultez les autres kernels pour obtenir des informations (ou posez des questions).

  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 dans une fonction et que vous avez une sortie d'erreur, 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 de mémoire en suspens lorsque TF_LITE_OPAQUE_ENSURE est utilisé. En d'autres termes, ces macros doivent être utilisées avant l'allocation de ressources qui fuiront.