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

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

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

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

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

האופרטור TensorFlow Text הוא דוגמה לאופרטור בהתאמה אישית. לדוגמה, אפשר לעיין במדריך בנושא המרת טקסט TF ל-TF Lite.

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

נראה דוגמה לתמיכה במפעיל TensorFlow שלא קיים ב-TensorFlow Lite. נניח שאנחנו משתמשים באופרטור 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

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

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

המרה למודל TensorFlow Lite

כדי ליצור מודל TensorFlow Lite עם אופרטורים מותאמים אישית, מגדירים את מאפיין הממיר 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"

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

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

typedef struct TfLiteRegistrationExternal TfLiteRegistrationExternal;

TfLiteRegistrationExternal שומר את זהות המפעיל וההטמעה שלו. (חשוב לשים לב שהאופרטור שונה מהאופרנדים שלו, שמאוחסנים בצמתים של הגרף ב-TF Lite בצמתים שקוראים לאופרטור).

מכונות מהסוג הזה נבנות באמצעות קריאות ל-TfLiteRegistrationExternalCreate ואפשר להשמיד אותן באמצעות קריאה ל-TfLiteRegistrationExternalDelete.

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

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

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

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

למידע נוסף על TfLiteContext ועל TfLiteNode, קראו את common.h. TfLiteContext מספק מתקני דיווח על שגיאות וגישה לאובייקטים גלובליים, כולל כל רכיבי Tensor. השיטה TfLiteNode מאפשרת להטמעות של אופרטורים לגשת לקלט ולפלט שלהם.

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

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

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

כדי להטמיע פעולות מותאמות אישית, צריך להגדיר את פונקציות ה-method האלה, ולאחר מכן להגדיר פונקציה שמחזירה מופע של TfLiteRegistrationExternal שנוצר על ידי קריאה ל-TfLiteRegistrationExternalCreate ואז לשיטות ה-setter הרלוונטיות:

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

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

הגדרת הליבה בסביבת זמן הריצה של TensorFlow Lite

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

כשמאתחלים את OpResolver, מוסיפים את ההפעלה המותאמת אישית למקודד (דוגמה בהמשך). הפעולה הזו תרשום את המפעיל ב-Tensorflow Lite כדי ש-TensorFlow Lite יוכל להשתמש בהטמעה החדשה. שים לב ששני הארגומנטים האחרונים ב-TfLiteRegistration תואמים לפונקציות AtanPrepare ו-AtanEval שהגדרת לפעולה המותאמת אישית. אם השתמשת בפונקציות AtanInit ו-AtanFree כדי לאתחל משתנים המשמשים בפעולת הפעולה ולפנות מקום, בהתאמה, הם יתווספו לשני הארגומנטים הראשונים של TfLiteRegistration; הארגומנטים האלה מוגדרים כ-nullptr בדוגמה הזו.

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

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

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

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

שים לב שלתאימות לאחור, המחלקה הזו משתמשת בסוג הבטון הישן TfLiteRegistration במקום בסוג האטום TfLiteRegistrationExternal, אבל ה-struct TfLiteRegistration מכיל שדה registration_external מסוג TfLiteRegistrationExternal*.

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

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

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

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

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

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

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

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

בדיקת ספק השירות והוספתו לפרופיל

כדי ליצור פרופיל לפעילות שלכם באמצעות כלי ההשוואה לשוק TensorFlow Lite, אתם יכולים להשתמש בכלי ליצירת מודלים במבחן השוואת ביצועים ב-TensorFlow Lite. למטרות בדיקה, אפשר ליידע את גרסת ה-build המקומית של TensorFlow Lite בפעולה המותאמת אישית שלך על ידי הוספת הקריאה המתאימה ל-AddCustom (כפי שמתואר למעלה) ל-register.cc

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

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

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

    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, כלומר, צריך להשתמש בפקודות המאקרו האלה לפני שמקצים משאבים שדלפו.