מכשיר LiteRT הענקת גישה מאפשרת לך להריץ את המודלים (חלקיים או שלמים) על קובץ הרצה אחר. המנגנון הזה יכול למנף מגוון מאיצים במכשיר, כמו GPU או Edge TPU (Tensor) יחידת עיבוד) להֶקֵּשׁ. המודל הזה מספק למפתחים להיות מופרדת מ-TFLite כברירת מחדל כדי לזרז את ההסקה.
התרשים הבא מסכם את המשתמשים שקיבלו הרשאה. פרטים נוספים מופיעים בקטעים שבהמשך.
מתי צריך ליצור נציג מורשה בהתאמה אישית?
ב-LiteRT יש מגוון רחב של משתמשים עם הרשאה להאצת יעדים, כמו GPU , DSP ו-EdgeTPU.
כדאי ליצור בעל גישה משלכם בתרחישים הבאים:
- אתם רוצים לשלב מנוע מסקנות חדש של למידת מכונה שלא נתמך על ידי אף משתמש אחר
- יש לכם מאיץ חומרה מותאם אישית שמשפר את זמן הריצה במקרים מסוימים.
- אתם מפתחים אופטימיזציות של המעבד (CPU) (כמו פיוז'ן אופרטורים) שיכולות כדי להאיץ מודלים מסוימים.
איך פועלים המשתמשים שקיבלו הרשאה?
קחו לדוגמה תרשים מודל פשוט כמו הדוגמה הבאה, ושאליו "MyDelegate" השיטה הזו מאפשרת הטמעה מהירה יותר של פעולות Conv2D ו-Mean.
לאחר החלת ה-"MyDelegate", תרשים LiteRT המקורי יהיה עודכן באופן הבא:
הגרף שלמעלה נוצר כאשר LiteRT מפצל את הגרף המקורי שני הכללים הבאים:
- פעולות ספציפיות שהמקבל יכול לטפל בהן מועברות ועדיין לעמוד בתהליך המחשוב המקורי של יחסי התלות בין הפעולות.
- לכל מחיצה להקצאה יש רק צומתי קלט ופלט שאינם מטופל על ידי מקבל הגישה.
כל מחיצה שמטופלת על ידי מקבל הגישה מוחלפת בצומת של בעל הגישה (יכול להיות נקראת גם ליבה מואצלת) בגרף המקורי שמעריך את במחיצה בזמן הפעלת הקריאה שלה.
בהתאם למודל, התרשים הסופי יכול להסתיים בצומת אחד או יותר, כלומר, חלק מהפעולות לא נתמכות על ידי המשתמש עם ההרשאה. באופן כללי, אני לא רוצה שהמחיצות יטופלו על ידי אותו מחיצות, כי כל אחת כשעוברים מהתכונה 'הענקת גישה' לתרשים הראשי, יש תקורה להעביר את התוצאות מהמשנה שהוענקה לתרשים הראשי, בגלל עותקים של הזיכרון (לדוגמה, מ-GPU למעבד (CPU). תקורה כזו עלולה לקזז את משתפרת במיוחד כשיש כמות גדולה של עותקים בזיכרון.
הטמעה של נציג משלכם בהתאמה אישית
השיטה המועדפת להוספת נציג היא Simple שקשורים ל-API.
כדי ליצור נציג מורשה חדש, עליך להטמיע שני ממשקים ולספק את לבצע הטמעה עצמית של השיטות בממשק.
1 - SimpleDelegateInterface
הסיווג הזה מייצג את היכולות של בעל הגישה, שהן הפעולות נתמכת, וסיווג יצרן ליצירת ליבה (kernel) שכוללת תרשים שהוענקה הרשאה לנהל אותו. לפרטים נוספים, ראו את הממשק המוגדר כאן קובץ כותרת מסוג C++. ההערות בקוד מסבירות בפירוט כל API.
2 - SimpleDelegateKernelInterface
השיעור הזה כולל את הלוגיקה לאתחול / הכנה / והפעלה של למחיצה שהוקצתה לכם.
היא כוללת: (ראו הגדרה)
- Init(...): תתבצע קריאה פעם אחת לביצוע אתחול חד-פעמי.
- הכנה(...): בוצעה קריאה לכל מופע שונה של צומת זה - זה קורה אם יש לך מספר מחיצות מוקצות. בדרך כלל ברצונך להשתמש בזיכרון בהקצאות האלה, כי הוא ייקרא בכל פעם שיש שינוי גודל של מקטעים.
- הפעלה(...): תתבצע קריאה לצורך הסקת מסקנות.
דוגמה
בדוגמה הזו, תיצרו משתמש עם הרשאה פשוטה מאוד שיכול לתמוך רק ב-2 סוגי פעולות (ADD) ו-SUB (Sub) עם tensors מסוג float32 בלבד.
// MyDelegate implements the interface of SimpleDelegateInterface.
// This holds the Delegate capabilities.
class MyDelegate : public SimpleDelegateInterface {
public:
bool IsNodeSupportedByDelegate(const TfLiteRegistration* registration,
const TfLiteNode* node,
TfLiteContext* context) const override {
// Only supports Add and Sub ops.
if (kTfLiteBuiltinAdd != registration->builtin_code &&
kTfLiteBuiltinSub != registration->builtin_code)
return false;
// This delegate only supports float32 types.
for (int i = 0; i < node->inputs->size; ++i) {
auto& tensor = context->tensors[node->inputs->data[i]];
if (tensor.type != kTfLiteFloat32) return false;
}
return true;
}
TfLiteStatus Initialize(TfLiteContext* context) override { return kTfLiteOk; }
const char* Name() const override {
static constexpr char kName[] = "MyDelegate";
return kName;
}
std::unique_ptr<SimpleDelegateKernelInterface> CreateDelegateKernelInterface()
override {
return std::make_unique<MyDelegateKernel>();
}
};
בשלב הבא, יוצרים ליבה (kernel) משלכם להאצלת סמכויות על ידי קבלת בירושה
SimpleDelegateKernelInterface
// My delegate kernel.
class MyDelegateKernel : public SimpleDelegateKernelInterface {
public:
TfLiteStatus Init(TfLiteContext* context,
const TfLiteDelegateParams* params) override {
// Save index to all nodes which are part of this delegate.
inputs_.resize(params->nodes_to_replace->size);
outputs_.resize(params->nodes_to_replace->size);
builtin_code_.resize(params->nodes_to_replace->size);
for (int i = 0; i < params->nodes_to_replace->size; ++i) {
const int node_index = params->nodes_to_replace->data[i];
// Get this node information.
TfLiteNode* delegated_node = nullptr;
TfLiteRegistration* delegated_node_registration = nullptr;
TF_LITE_ENSURE_EQ(
context,
context->GetNodeAndRegistration(context, node_index, &delegated_node,
&delegated_node_registration),
kTfLiteOk);
inputs_[i].push_back(delegated_node->inputs->data[0]);
inputs_[i].push_back(delegated_node->inputs->data[1]);
outputs_[i].push_back(delegated_node->outputs->data[0]);
builtin_code_[i] = delegated_node_registration->builtin_code;
}
return kTfLiteOk;
}
TfLiteStatus Prepare(TfLiteContext* context, TfLiteNode* node) override {
return kTfLiteOk;
}
TfLiteStatus Eval(TfLiteContext* context, TfLiteNode* node) override {
// Evaluate the delegated graph.
// Here we loop over all the delegated nodes.
// We know that all the nodes are either ADD or SUB operations and the
// number of nodes equals ''inputs_.size()'' and inputs[i] is a list of
// tensor indices for inputs to node ''i'', while outputs_[i] is the list of
// outputs for node
// ''i''. Note, that it is intentional we have simple implementation as this
// is for demonstration.
for (int i = 0; i < inputs_.size(); ++i) {
// Get the node input tensors.
// Add/Sub operation accepts 2 inputs.
auto& input_tensor_1 = context->tensors[inputs_[i][0]];
auto& input_tensor_2 = context->tensors[inputs_[i][1]];
auto& output_tensor = context->tensors[outputs_[i][0]];
TF_LITE_ENSURE_EQ(
context,
ComputeResult(context, builtin_code_[i], &input_tensor_1,
&input_tensor_2, &output_tensor),
kTfLiteOk);
}
return kTfLiteOk;
}
private:
// Computes the result of addition of 'input_tensor_1' and 'input_tensor_2'
// and store the result in 'output_tensor'.
TfLiteStatus ComputeResult(TfLiteContext* context, int builtin_code,
const TfLiteTensor* input_tensor_1,
const TfLiteTensor* input_tensor_2,
TfLiteTensor* output_tensor) {
if (NumElements(input_tensor_1) != NumElements(input_tensor_2) ||
NumElements(input_tensor_1) != NumElements(output_tensor)) {
return kTfLiteDelegateError;
}
// This code assumes no activation, and no broadcasting needed (both inputs
// have the same size).
auto* input_1 = GetTensorData<float>(input_tensor_1);
auto* input_2 = GetTensorData<float>(input_tensor_2);
auto* output = GetTensorData<float>(output_tensor);
for (int i = 0; i < NumElements(input_tensor_1); ++i) {
if (builtin_code == kTfLiteBuiltinAdd)
output[i] = input_1[i] + input_2[i];
else
output[i] = input_1[i] - input_2[i];
}
return kTfLiteOk;
}
// Holds the indices of the input/output tensors.
// inputs_[i] is list of all input tensors to node at index 'i'.
// outputs_[i] is list of all output tensors to node at index 'i'.
std::vector<std::vector<int>> inputs_, outputs_;
// Holds the builtin code of the ops.
// builtin_code_[i] is the type of node at index 'i'
std::vector<int> builtin_code_;
};
בוחנים ובוחנים את הגישה למשתמש החדש
ל-TFLite יש ערכת כלים שאפשר לבדוק במהירות בהשוואה למודל TFLite.
- כלי ההשוואה לשוק: הכלי משתמש במודל TFLite, יוצר קלט אקראי ולאחר מכן מפעיל את המודל עבור מספר נתון של הפעלות. היא מדפיסה זמן אחזור מצטבר נתונים סטטיסטיים בסוף.
- הכלי 'הבדלים בהסקה': עבור מודל נתון, הכלי מפיק נתונים גאוסיאניים אקראיים ומעביר אותם באמצעות שני מתורגמים שונים של TFLite, שאחד מהם מפעיל מעבד (CPU) עם שרשור יחיד הליבה והשני באמצעות מפרט מוגדר על ידי המשתמש. הוא מודד את הערך המוחלט את ההבדל בין מעבדי הפלט לכל מתרגם, לכל רכיב. הכלי הזה יכול לעזור גם לשפר את רמת הדיוק של ניפוי הבאגים בעיות נפוצות.
- יש גם כלי הערכה ספציפיים למשימה, לסיווג תמונות זיהוי אובייקטים. ניתן למצוא את הכלים האלה. כאן
בנוסף, ל-TFLite יש קבוצה גדולה של בדיקות ליבה ויחידות הפעלה, שיכולות נעשה שימוש חוזר כדי לבדוק את הנציג החדש עם כיסוי נוסף ולהבטיח נתיב הביצוע של קובץ TFLite לא שבור.
כדי לעשות שימוש חוזר בבדיקות TFLite ובכלים למשתמשים שקיבלו הרשאה, אפשר להשתמש אחת משתי האפשרויות הבאות:
- משתמשים ב- הרשם האחראי על הגישה על מנגנוני תשומת לב.
- משתמשים ב- נציג חיצוני על מנגנוני תשומת לב.
בחירת הגישה הטובה ביותר
בשתי הגישות נדרשים כמה שינויים כמפורט בהמשך. אבל, בהערה הראשונה מקשרת את בעל הגישה באופן סטטי ומחייבת בנייה מחדש של הבדיקה, כלים להשוואה ולהערכה. לעומת זאת, המודל השני גורם להעניק גישה כספרייה משותפת ודורשת ממך לחשוף את קטע היצירה/מחיקה מהספרייה המשותפת.
כתוצאה מכך, המנגנון להענקת גישה חיצונית יפעל עם קבצים בינאריים מוכנים מראש של כלים של LiteRT. אבל היא פחות בוטה ויכול להיות שיהיה מסובך יותר להגדיר אותה באופן אוטומטי בדיקות שילוב. לשיפור הבהירות, כדאי להשתמש בגישה של הרשם בהענקת גישה.
אפשרות 1: שימוש ברשם של הרשאות הגישה
הרשם האחראי על הגישה שומר רשימה של ספקים עם הרשאות גישה, שכל אחד מהם מספק דרך קלה ליצור למשתמשי TFLite שמבוססים על דגלי שורת הפקודה, ולכן הם נוחים לשימוש ובכלים. כדי לחבר את המשתמש החדש לכל כלי LiteRT שהוזכרו קודם כול, צריך ליצור ספק בעל גישה חדש, לבצע רק מעט שינויים בכללי BUILD. דוגמה מלאה לכך תהליך השילוב מוצג בהמשך (וניתן למצוא את הקוד כאן).
בהנחה שיש לכם משתמש מורשה שמיישם את ממשקי ה-API של Simpleהענקת, extern "C" ממשקי API של יצירה/מחיקה של 'dummy' זה להאציל כפי שמוצג בהמשך:
// Returns default options for DummyDelegate.
DummyDelegateOptions TfLiteDummyDelegateOptionsDefault();
// Creates a new delegate instance that need to be destroyed with
// `TfLiteDummyDelegateDelete` when delegate is no longer used by TFLite.
// When `options` is set to `nullptr`, the above default values are used:
TfLiteDelegate* TfLiteDummyDelegateCreate(const DummyDelegateOptions* options);
// Destroys a delegate created with `TfLiteDummyDelegateCreate` call.
void TfLiteDummyDelegateDelete(TfLiteDelegate* delegate);
כדי לשלב את 'DummyDelegate' עם כלי ההשוואה וכלי ההסקה, מגדירים StreamProvider כמו שמצוין בהמשך:
class DummyDelegateProvider : public DelegateProvider {
public:
DummyDelegateProvider() {
default_params_.AddParam("use_dummy_delegate",
ToolParam::Create<bool>(false));
}
std::vector<Flag> CreateFlags(ToolParams* params) const final;
void LogParams(const ToolParams& params) const final;
TfLiteDelegatePtr CreateTfLiteDelegate(const ToolParams& params) const final;
std::string GetName() const final { return "DummyDelegate"; }
};
REGISTER_DELEGATE_PROVIDER(DummyDelegateProvider);
std::vector<Flag> DummyDelegateProvider::CreateFlags(ToolParams* params) const {
std::vector<Flag> flags = {CreateFlag<bool>("use_dummy_delegate", params,
"use the dummy delegate.")};
return flags;
}
void DummyDelegateProvider::LogParams(const ToolParams& params) const {
TFLITE_LOG(INFO) << "Use dummy test delegate : ["
<< params.Get<bool>("use_dummy_delegate") << "]";
}
TfLiteDelegatePtr DummyDelegateProvider::CreateTfLiteDelegate(
const ToolParams& params) const {
if (params.Get<bool>("use_dummy_delegate")) {
auto default_options = TfLiteDummyDelegateOptionsDefault();
return TfLiteDummyDelegateCreateUnique(&default_options);
}
return TfLiteDelegatePtr(nullptr, [](TfLiteDelegate*) {});
}
ההגדרות של כללי BUILD חשובות כי עליך לוודא הספרייה תמיד מקושרת והכלי האופטימיזציה לא מושמט.
#### The following are for using the dummy test delegate in TFLite tooling ####
cc_library(
name = "dummy_delegate_provider",
srcs = ["dummy_delegate_provider.cc"],
copts = tflite_copts(),
deps = [
":dummy_delegate",
"//tensorflow/lite/tools/delegates:delegate_provider_hdr",
],
alwayslink = 1, # This is required so the optimizer doesn't optimize the library away.
)
עכשיו צריך להוסיף את שני כללי ה-wrapper האלה לקובץ ה-BUILD כדי ליצור גרסה של כלי ההשוואה לשוק, כלי ההסקה וכלי הערכה אחרים, שיכולים לפעול עם נציג משלכם.
cc_binary(
name = "benchmark_model_plus_dummy_delegate",
copts = tflite_copts(),
linkopts = task_linkopts(),
deps = [
":dummy_delegate_provider",
"//tensorflow/lite/tools/benchmark:benchmark_model_main",
],
)
cc_binary(
name = "inference_diff_plus_dummy_delegate",
copts = tflite_copts(),
linkopts = task_linkopts(),
deps = [
":dummy_delegate_provider",
"//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
"//tensorflow/lite/tools/evaluation/tasks/inference_diff:run_eval_lib",
],
)
cc_binary(
name = "imagenet_classification_eval_plus_dummy_delegate",
copts = tflite_copts(),
linkopts = task_linkopts(),
deps = [
":dummy_delegate_provider",
"//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
"//tensorflow/lite/tools/evaluation/tasks/imagenet_image_classification:run_eval_lib",
],
)
cc_binary(
name = "coco_object_detection_eval_plus_dummy_delegate",
copts = tflite_copts(),
linkopts = task_linkopts(),
deps = [
":dummy_delegate_provider",
"//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
"//tensorflow/lite/tools/evaluation/tasks/coco_object_detection:run_eval_lib",
],
)
אפשר גם לחבר את הספק בעל הגישה הזה לבדיקות ליבה (kernel) של TFLite, כפי שמתואר. כאן.
אפשרות 2: מינוף של משתמש חיצוני
בחלופה הזו, קודם יוצרים מתאם חיצוני להענקת גישה external_delegate_adaptor.cc כפי שמוצג בהמשך. שימו לב, הגישה הזו קצת פחות מועדפת בהשוואה אפשרות 1, כפי שהוזכר למעלה.
TfLiteDelegate* CreateDummyDelegateFromOptions(char** options_keys,
char** options_values,
size_t num_options) {
DummyDelegateOptions options = TfLiteDummyDelegateOptionsDefault();
// Parse key-values options to DummyDelegateOptions.
// You can achieve this by mimicking them as command-line flags.
std::unique_ptr<const char*> argv =
std::unique_ptr<const char*>(new const char*[num_options + 1]);
constexpr char kDummyDelegateParsing[] = "dummy_delegate_parsing";
argv.get()[0] = kDummyDelegateParsing;
std::vector<std::string> option_args;
option_args.reserve(num_options);
for (int i = 0; i < num_options; ++i) {
option_args.emplace_back("--");
option_args.rbegin()->append(options_keys[i]);
option_args.rbegin()->push_back('=');
option_args.rbegin()->append(options_values[i]);
argv.get()[i + 1] = option_args.rbegin()->c_str();
}
// Define command-line flags.
// ...
std::vector<tflite::Flag> flag_list = {
tflite::Flag::CreateFlag(...),
...,
tflite::Flag::CreateFlag(...),
};
int argc = num_options + 1;
if (!tflite::Flags::Parse(&argc, argv.get(), flag_list)) {
return nullptr;
}
return TfLiteDummyDelegateCreate(&options);
}
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
// Defines two symbols that need to be exported to use the TFLite external
// delegate. See tensorflow/lite/delegates/external for details.
TFL_CAPI_EXPORT TfLiteDelegate* tflite_plugin_create_delegate(
char** options_keys, char** options_values, size_t num_options,
void (*report_error)(const char*)) {
return tflite::tools::CreateDummyDelegateFromOptions(
options_keys, options_values, num_options);
}
TFL_CAPI_EXPORT void tflite_plugin_destroy_delegate(TfLiteDelegate* delegate) {
TfLiteDummyDelegateDelete(delegate);
}
#ifdef __cplusplus
}
#endif // __cplusplus
עכשיו יוצרים את יעד BUILD המתאים כדי ליצור ספרייה דינמית כמו שמוצג למטה:
cc_binary(
name = "dummy_external_delegate.so",
srcs = [
"external_delegate_adaptor.cc",
],
linkshared = 1,
linkstatic = 1,
deps = [
":dummy_delegate",
"//tensorflow/lite/c:common",
"//tensorflow/lite/tools:command_line_flags",
"//tensorflow/lite/tools:logging",
],
)
לאחר יצירת קובץ ה- .so מוכנים מראש לרוץ עם מקבל הגישה החדש, כל עוד הקובץ הבינארי מקושר ה external_delegate_provider שתומכת בדגלים של שורת הפקודה, כפי שמתואר כאן. הערה: הספק החיצוני הזה שהוענקה לך גישה אליו כבר קושר לחשבון קיים קבצים בינאריים של בדיקה וכלים.
עיון בתיאורים כאן איור של איך לקבוע נקודת השוואה לנציג הדמה להענקת גישה חיצונית. אפשר להשתמש בפקודות דומות לצורך הבדיקה בכלי ההערכה שהזכרנו קודם.
כדאי לציין שמקבל הגישה החיצונית הוא ה-C++ המתאים של ההענקת גישה בקישור LiteRT Python כפי שמוצג כאן. לכן, ספריית המתאם החיצונית הדינמית להענקת גישה שנוצרה כאן יכולה להיות בשימוש ישיר עם ממשקי API של LiteRT Python.
משאבים
הורדת קישורים לקבצים בינאריים של כלים בינאריים מוכנים מראש של TFLite
מערכת ההפעלה | ARCH | BINARY_NAME |
Linux | x86_64 | |
זרוע | ||
aarch64 | ||
Android | זרוע | |
aarch64 |