הטמעה של התכונה 'הענקת גישה בהתאמה אישית'

הענקת גישה ל-TensorFlow Lite מאפשרת לכם להריץ את המודלים שלכם (חלקיים או שלמים) במנהל אחר. המנגנון הזה יכול למנף מגוון של מאיצים במכשיר, כמו GPU או Edge TPU (יחידת עיבוד Tensor) לצורך הסקת מידע. כך המפתחים מקבלים שיטה גמישה ונפרדת מברירת המחדל של TFLite כדי לזרז את ההסקה.

בתרשים הבא מוצג סיכום של המשתמשים שקיבלו הרשאה. פרטים נוספים מופיעים בקטעים הבאים.

צירי TFLite

מתי צריך ליצור משתמש שקיבל גישה בהתאמה אישית?

ל-TensorFlow Lite יש מגוון רחב של נציגים שמיועדים להאצה של יעדים כמו GPU, DSP ו-EdgeTPU.

יצירת נציג מורשה משלכם מועילה בתרחישים הבאים:

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

איך עובדים נציגים מורשים?

כדאי להשתמש בתרשים מודל פשוט כמו בדוגמה הבאה, ולהאציל 'MyDelegate' שיש לו הטמעה מהירה יותר עבור פעולות Conv2D ו-Mean.

התרשים המקורי

לאחר החלת ה-MyDelegate, הגרף המקורי של TensorFlow Lite יעודכן באופן הבא:

תרשים עם נציג

התרשים שלמעלה מתקבל כש-TensorFlow Lite מפצל את התרשים המקורי לפי שני כללים:

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

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

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

הטמעת משתמש עם הרשאה בהתאמה אישית

הדרך המועדפת להוספת הענקת גישה היא באמצעות SimpleDelegate API.

כדי לאפשר גישה חדשה, אתם צריכים להטמיע 2 ממשקים ולספק הטמעה משלכם לשיטות הממשק.

1 - SimpleDelegateInterface

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

2 - SimpleDelegateKernelInterface

בשיעור הזה מופיע הלוגיקה לאתחול / הכנה / להפעלה של המחיצה שהוענקה.

יש בו: (ראו הגדרה)

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

דוגמה

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

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

בחירת הגישה הטובה ביותר

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

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

אפשרות 1: שימוש ברשם מואצל

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

בהנחה שיש לך הרשאה שמטמיעה את ממשקי ה-API של SimpleDelegate, וממשקי ה-API החיצוניים "C" של יצירה/מחיקה של משתמש דמה זה, כפי שמוצג בהמשך:

// 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' עם כלי הבנצ'מרק ואת כלי ההֶקֵּשׁ, צריך להגדיר DelegateProvider כמו בדוגמה הבאה:

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",
    ],
)

אתם גם יכולים לחבר את הספק הזה לבדיקות של ליבת 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++ התואמת של האצלה בקישור ל-Python ב-Tensorflow Lite כפי שמוצג כאן. לכן אפשר להשתמש ישירות בספריית המתאם הדינמית החיצונית של המתאם שנוצרת כאן, יחד עם ממשקי ה-API של Tensorflow Lite Python.

משאבים

מערכת ההפעלה קשת BINARY_NAME
Linux x86_64
זרוע
aarch64
Android זרוע
aarch64