ספריית האופרטורים המובנית של LiteRT תומכת רק במספר מוגבל של אופרטורים של TensorFlow, ולכן לא כל מודל ניתן להמרה. פרטים נוספים זמינים במאמר בנושא תאימות למפעילים.
כדי לאפשר המרה, המשתמשים יכולים לספק הטמעה מותאמת אישית משלהם של אופרטור TensorFlow שלא נתמך ב-LiteRT, שנקרא אופרטור מותאם אישית. אם רוצים לשלב סדרה של אופרטורים של TensorFlow שלא נתמכים (או שנתמכים) באופרטור מותאם אישית יחיד וממוטב, אפשר לעיין במאמר בנושא מיזוג אופרטורים.
השימוש באופרטורים מותאמים אישית כולל ארבעה שלבים.
יוצרים מודל TensorFlow. מוודאים שהמודל השמור (או Graph Def) מתייחס לאופרטור LiteRT עם השם הנכון.
המרת המודל למודל LiteRT. כדי להמיר את המודל בהצלחה, חשוב לוודא שהגדרתם את מאפיין ההמרה הנכון של LiteRT.
יוצרים ומגדירים את האופרטור. הסיבה לכך היא שסביבת הריצה של LiteRT צריכה לדעת איך למפות את האופרטור והפרמטרים בתרשים לקוד C/C++ שניתן להפעלה.
בדיקה של המפעיל ופרופיל שלו אם רוצים לבדוק רק את האופרטור המותאם אישית, מומלץ ליצור מודל עם האופרטור המותאם אישית בלבד ולהשתמש בתוכנית benchmark_model.
בואו נראה דוגמה מלאה להרצת מודל עם אופרטור מותאם אישית 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
, שמוגדרים על ידי העברת הכתובות של פונקציות ההטמעה לפונקציות ההגדרה המתאימות TfLiteOperatorSet
MethodName:
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 תזהה את האופרטור המותאם אישית.
שיטות מומלצות
חשוב לבצע אופטימיזציה של הקצאות זיכרון וביטול הקצאות בזהירות. הקצאת זיכרון ב-
Prepare
יעילה יותר מאשר ב-Invoke
, והקצאת זיכרון לפני לולאה עדיפה על הקצאת זיכרון בכל איטרציה. עדיף להשתמש בנתוני טנסורים זמניים במקום להקצות זיכרון בעצמכם (ראו פריט 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; }
אם זה לא גורם לבזבוז רב מדי של זיכרון, עדיף להשתמש במערך סטטי בגודל קבוע (או ב-
std::vector
שהוקצה מראש ב-Resize
) במקום להשתמש ב-std::vector
שהוקצה באופן דינמי בכל איטרציה של הביצוע.כדאי להימנע מהפעלת תבניות של קונטיינרים בספרייה רגילה שלא קיימות כבר, כי הן משפיעות על הגודל של הקובץ הבינארי. לדוגמה, אם אתם צריכים את הפעולה
std::map
שלא קיימת בקרנלים אחרים, יכול להיות ששימוש ב-std::vector
עם מיפוי אינדקס ישיר יתאים לכם ויאפשר לכם לשמור על גודל קטן של הקובץ הבינארי. אפשר לראות באילו ליבות אחרות נעשה שימוש כדי לקבל תובנות (או לשאול).בודקים את המצביע לזיכרון שמוחזר על ידי
malloc
. אם המצביע הזה הואnullptr
, אין לבצע פעולות באמצעות המצביע הזה. אם אתםmalloc
בפונקציה ויש לכם יציאה עם שגיאה, בטלו את הקצאת הזיכרון לפני שאתם יוצאים.אפשר להשתמש ב-
TF_LITE_OPAQUE_ENSURE(context, condition)
כדי לבדוק תנאי ספציפי. הקוד לא יכול להשאיר זיכרון תלוי כשמשתמשים ב-TF_LITE_OPAQUE_ENSURE
, כלומר, צריך להשתמש בפקודות המאקרו האלה לפני הקצאה של משאבים שעלולים לגרום לדליפת זיכרון.