אופרטורים מותאמים אישית

ספריית האופרטורים המובנית של LiteRT תומכת רק במספר מוגבל של אופרטורים של TensorFlow, ולכן לא כל מודל ניתן להמרה. פרטים נוספים זמינים במאמר בנושא תאימות למפעילים.

כדי לאפשר המרה, המשתמשים יכולים לספק הטמעה מותאמת אישית משלהם של אופרטור TensorFlow שלא נתמך ב-LiteRT, שנקרא אופרטור מותאם אישית. אם רוצים לשלב סדרה של אופרטורים של TensorFlow שלא נתמכים (או שנתמכים) באופרטור מותאם אישית יחיד וממוטב, אפשר לעיין במאמר בנושא מיזוג אופרטורים.

השימוש באופרטורים מותאמים אישית כולל ארבעה שלבים.

בואו נראה דוגמה מלאה להרצת מודל עם אופרטור מותאם אישית tf.atan (בשם Atan, אפשר לעיין במאמר יצירת מודל TensorFlow) שנתמך ב-TensorFlow, אבל לא נתמך ב-LiteRT.

האופרטור TensorFlow Text הוא דוגמה לאופרטור בהתאמה אישית. דוגמת קוד זמינה במדריך בנושא המרת טקסט TF ל-LiteRT.

דוגמה: אופרטור מותאם אישית Atan

נסביר בעזרת דוגמה איך אפשר לתמוך באופרטור TensorFlow שאין ב-LiteRT. נניח שאנחנו משתמשים באופרטור Atan ויוצרים מודל פשוט מאוד לפונקציה y = atan(x + offset), שבה offset ניתן לאימון.

יצירת מודל TensorFlow

בקטע הקוד הבא מתבצע אימון של מודל TensorFlow פשוט. המודל הזה מכיל רק אופרטור בהתאמה אישית בשם Atan, שהוא פונקציה y = atan(x + offset), שבה offset ניתן לאימון.

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

בשלב הזה, אם תנסו ליצור מודל LiteRT עם דגלי ההמרה שמוגדרים כברירת מחדל, תוצג הודעת השגיאה הבאה:

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

המרת המודל ל-LiteRT

כדי ליצור מודל LiteRT עם אופרטורים בהתאמה אישית, מגדירים את מאפיין ההמרה allow_custom_ops כמו שמוצג בהמשך:

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

בשלב הזה, אם מריצים את הפקודה עם המפרש שמוגדר כברירת מחדל באמצעות פקודות כמו:

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

השגיאה עדיין תופיע:

Encountered unresolved custom op: Atan.

יוצרים את האופרטור ורושמים אותו.

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

אופרטורים מותאמים אישית של LiteRT מוגדרים באמצעות API פשוט של C טהור, שמורכב מסוג אטום (TfLiteOperator) ופונקציות קשורות.

TfLiteOperator הוא סוג אטום:

typedef struct TfLiteOperator TfLiteOperator;

TfLiteOperator מאחסן את הזהות וההטמעה של המפעיל. (שימו לב שהאופרטור שונה מהאופרנדים שלו, שמאוחסנים בצמתי הגרף של LiteRT עבור צמתים שמפעילים את האופרטור).

מופעים מהסוג הזה נוצרים באמצעות קריאות ל-TfLiteOperatorCreate ואפשר להרוס אותם באמצעות קריאה ל-TfLiteOperatorDelete.

הזהות של האופרטור מוגדרת באמצעות הפרמטרים של פונקציית ה-constructor‏ 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.
);

ההטמעה של האופרטור יכולה להגדיר 'שיטות' עם החתימות הבאות. כל השיטות האלה הן אופציונליות, אבל כדי שאפשר יהיה להעריך את האופרטור בהצלחה, בהטמעה של האופרטור צריך להגדיר ולהגדיר (באמצעות פונקציות setter) לפחות את השיטות Prepare ו-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 (או קידומות של מרחב שמות, ב-C++) בהטמעה של האופרטור לא צריכים להיות זהים לשמות הפונקציות בקטע הקוד שלמעלה, כי ה-API של אופרטורים מותאמים אישית ב-TF Lite ישתמש רק בכתובות שלהם. למעשה, מומלץ להצהיר עליהן במרחב שמות אנונימי או כפונקציות סטטיות.

אבל מומלץ לכלול את שם הספק כמרחב שמות או כקידומת בשמות הפונקציות האלה:

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.
      

מכיוון שמדובר בממשק API של C, ה'שיטות' האלה מיושמות כמצביעים לפונקציות C בסוג TfLiteOperator, שמוגדרים על ידי העברת הכתובות של פונקציות ההטמעה לפונקציות ההגדרה המתאימות 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));

פרטים נוספים על TfLiteContext ועל TfLiteNode זמינים במאמר common.h. ‫TfLiteContext מספק כלים לדיווח על שגיאות וגישה לאובייקטים גלובליים, כולל כל הטנסורים. ה-API‏ TfLiteNode מאפשר להטמעות של אופרטורים לגשת לקלט ולפלט שלהם.

כשהמתורגמן טוען מודל, הוא קורא לשיטה Init() פעם אחת לכל צומת בתרשים. אם משתמשים באותו Init() כמה פעמים בתרשים, הוא יופעל יותר מפעם אחת. לגבי פעולות מותאמות אישית, יינתן מאגר תצורה שמכיל flexbuffer שממפה שמות של פרמטרים לערכים שלהם. המאגר ריק עבור פעולות מוכללות כי המפענח כבר ניתח את הפרמטרים של הפעולה. הטמעות של ליבת המערכת שדורשות מצב צריכות לאתחל אותו כאן ולהעביר את הבעלות למתקשר. לכל שיחה של Init() תהיה שיחה תואמת של Free(), שתאפשר להטמעות להיפטר מהמאגר שאולי הוקצה להן ב-Init().

בכל פעם שמבצעים שינוי בגודל של טנסורים של קלט, המפענח יעבור על הגרף ויעדכן את ההטמעות לגבי השינוי. כך הם יכולים לשנות את הגודל של המאגר הפנימי, לבדוק את התוקף של צורות וסוגים של קלט ולחשב מחדש את צורות הפלט. הכול מתבצע באמצעות השיטה Prepare(), והטמעות יכולות לגשת למצב שלהן באמצעות TfLiteOpaqueNodeGetUserData(node).

לבסוף, בכל פעם שההסקה מופעלת, המפרש עובר על הגרף ומפעיל את ה-method‏ Invoke(), וגם כאן המצב זמין בתור TfLiteOpaqueNodeGetUserData(node).

אפשר להטמיע פעולות בהתאמה אישית על ידי הגדרת הפונקציות האלה של 'שיטה', ואז להגדיר פונקציה שמחזירה מופע של TfLiteOperator שנבנה על ידי קריאה ל-TfLiteOperatorCreate ואז לשיטות ההגדרה הרלוונטיות:

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

שימו לב שהרישום לא מתבצע באופן אוטומטי, וצריך לבצע קריאה מפורשת לפונקציה MyCustomOperator (פרטים בהמשך). בזמן שהיעד הרגיל BuiltinOpResolver (שזמין מהיעד :builtin_ops) מטפל ברישום של פונקציות מובנות, צריך לאסוף אופרטורים מותאמים אישית בספריות מותאמות אישית נפרדות.

הגדרת ליבת ה-LiteRT בזמן הריצה

כדי להשתמש באופרטור ב-LiteRT, צריך להגדיר שתי פונקציות (Prepare ו-Eval) ופונקציה שלישית ליצירת 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;
}
      

כשמפעילים את OpResolver, מוסיפים את האופרטור המותאם אישית ל-resolver (דוגמה בהמשך). הפעולה הזו תרשום את האופרטור ב-LiteRT כדי ש-LiteRT יוכל להשתמש בהטמעה החדשה.

רישום האופרטור בספריית הליבה

עכשיו צריך לרשום את האופרטור בספריית הליבה. הפעולה הזו מתבצעת באמצעות OpResolver. מאחורי הקלעים, המפענח יטען ספרייה של ליבות שיקצו לביצוע כל אחד מהאופרטורים במודל. ספריית ברירת המחדל מכילה רק ליבות מובנות, אבל אפשר להחליף אותה בספרייה מותאמת אישית של אופרטורים או להוסיף לה אופרטורים.

המחלקות OpResolver, שמתרגמות קודים ושמות של אופרטורים לקוד בפועל, מוגדרות כך:

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

שימו לב: כדי לשמור על תאימות לאחור, המחלקה הזו משתמשת בסוג הקונקרטי הישן TfLiteRegistration ולא בסוג האטום TfLiteOperator, אבל המבנה TfLiteRegistration מכיל שדה registration_external מסוג TfLiteOperator*.

המחלקות MutableOpResolver ו-BuiltinOpResolver נגזרות מ-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.
};

שימוש רגיל (ללא פעולות מותאמות אישית) מחייב שימוש ב-BuiltinOpResolver וכתיבה של:

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

כדי להוסיף את פעולת ההתאמה האישית שנוצרה למעלה, אפשר להשתמש במקום זאת ב-MutableOpResolver ולקרוא ל-tflite::AddOp (לפני שמעבירים את הפותר ל-InterpreterBuilder):

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

אם קבוצת הפעולות המובנות גדולה מדי, אפשר ליצור קובץ OpResolver חדש באמצעות גנרטור קודים על סמך קבוצת משנה נתונה של פעולות, אולי רק הפעולות שכלולות במודל נתון. זה שווה ערך לרישום סלקטיבי של TensorFlow (גרסה פשוטה שלו זמינה בספרייה tools).

אם רוצים להגדיר אופרטורים מותאמים אישית ב-Java, צריך כרגע ליצור שכבת JNI מותאמת אישית משלכם ולבצע קומפילציה של AAR משלכם בקוד ה-JNI הזה. באופן דומה, אם רוצים להגדיר את האופרטורים האלה שזמינים ב-Python, אפשר למקם את הרישומים בקוד העטיפה של Python.

הערה: אפשר לבצע תהליך דומה לזה שמתואר למעלה כדי לתמוך בקבוצה של פעולות במקום באופרטור יחיד. פשוט מוסיפים כמה אופרטורים של AddCustom שצריך. בנוסף, MutableOpResolver מאפשרת גם לבטל הטמעות של פונקציות מובנות באמצעות AddBuiltin.

בדיקה ופרופיל של האופרטור

כדי ליצור פרופיל של האופרטור באמצעות כלי ההשוואה LiteRT, אפשר להשתמש בכלי להשוואת מודלים של LiteRT. לצורך בדיקה, אפשר להוסיף את הקריאה המתאימה ל-AddCustom (כמו שמוצג למעלה) ל-register.cc כדי שהגרסה המקומית של LiteRT תזהה את האופרטור המותאם אישית.

שיטות מומלצות

  1. חשוב לבצע אופטימיזציה של הקצאות זיכרון וביטול הקצאות בזהירות. הקצאת זיכרון ב-Prepare יעילה יותר מאשר ב-Invoke, והקצאת זיכרון לפני לולאה עדיפה על הקצאת זיכרון בכל איטרציה. עדיף להשתמש בנתוני טנסורים זמניים במקום להקצות זיכרון בעצמכם (ראו פריט 2). מומלץ להשתמש במצביעים או בהפניות במקום להעתיק כמה שיותר.

  2. אם מבנה הנתונים יישאר לאורך כל הפעולה, מומלץ להקצות מראש את הזיכרון באמצעות טנסורים זמניים. יכול להיות שתצטרכו להשתמש במבנה OpData כדי להפנות לאינדקסים של הטנסור בפונקציות אחרות. אפשר לראות את הדוגמה בליבת הקונבולוציה. בהמשך מופיע קטע קוד לדוגמה.

    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. אם זה לא גורם לבזבוז רב מדי של זיכרון, עדיף להשתמש במערך סטטי בגודל קבוע (או ב-std::vector שהוקצה מראש ב-Resize) במקום להשתמש ב-std::vector שהוקצה באופן דינמי בכל איטרציה של הביצוע.

  4. כדאי להימנע מהפעלת תבניות של קונטיינרים בספרייה רגילה שלא קיימות כבר, כי הן משפיעות על הגודל של הקובץ הבינארי. לדוגמה, אם אתם צריכים את הפעולה std::map שלא קיימת בקרנלים אחרים, יכול להיות ששימוש ב-std::vector עם מיפוי אינדקס ישיר יתאים לכם ויאפשר לכם לשמור על גודל קטן של הקובץ הבינארי. אפשר לראות באילו ליבות אחרות נעשה שימוש כדי לקבל תובנות (או לשאול).

  5. בודקים את המצביע לזיכרון שמוחזר על ידי malloc. אם המצביע הזה הוא nullptr, אין לבצע פעולות באמצעות המצביע הזה. אם אתם malloc בפונקציה ויש לכם יציאה עם שגיאה, בטלו את הקצאת הזיכרון לפני שאתם יוצאים.

  6. אפשר להשתמש ב-TF_LITE_OPAQUE_ENSURE(context, condition) כדי לבדוק תנאי ספציפי. הקוד לא יכול להשאיר זיכרון תלוי כשמשתמשים ב-TF_LITE_OPAQUE_ENSURE, כלומר, צריך להשתמש בפקודות המאקרו האלה לפני הקצאה של משאבים שעלולים לגרום לדליפת זיכרון.