יישום נציג מותאם אישית

מהו נציג TensorFlow Lite?

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

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

TFLite Delegates

מתי עלי ליצור נציג מותאם אישית?

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

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

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

איך עובדים הנציגים?

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

Original graph

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

Graph with delegate

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

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

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

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

יישום נציג מותאם אישית משלך

השיטה המועדפת להוספת נציג היא שימוש ב-SimpleDelegate API .

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

1 - SimpleDelegateInterface

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

2 - SimpleDelegateKernelInterface

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

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

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

דוגמא

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

  • Model Benchmark Tool : הכלי לוקח מודל TFLite, מייצר קלטות אקראיות, ולאחר מכן מריץ שוב ושוב את המודל עבור מספר מוגדר של ריצות. זה מדפיס סטטיסטיקות חביון מצטברות בסוף.
  • הכלי הבדל מסקנות : עבור מודל נתון, הכלי מייצר נתונים גאוסים אקראיים ומעביר אותם דרך שני מתורגמני TFLite שונים, האחד מריץ ליבת מעבד יחיד עם הברגה והשני משתמש במפרט מוגדר על ידי המשתמש. הוא מודד את ההבדל המוחלט בין טנסור הפלט מכל מתורגמן, על בסיס אלמנט. כלי זה יכול להועיל גם לאיתור בעיות דיוק.
  • ישנם גם כלי הערכה ספציפיים למשימה, לסיווג תמונות וזיהוי אובייקטים. כלים אלה ניתן למצוא כאן

בנוסף, ל-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" עם Benchmark Tool וכלי ההסקה, הגדר 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.
)

כעת הוסף את שני כללי המעטפת הללו בקובץ ה-BUILD שלך כדי ליצור גרסה של Benchmark Tool and Inference Tool, וכלי הערכה אחרים, שיכולים לפעול עם הנציג שלך.

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: מינוף נציג חיצוני

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

אֶמְצָעִי

מערכת הפעלה קֶשֶׁת BINARY_NAME
לינוקס x86_64
זְרוֹעַ
aarch64
דְמוּי אָדָם זְרוֹעַ
aarch64