تنفيذ مندوب مخصص

ما هو مندوب TensorFlow Lite؟

يتيح لك TensorFlow Lite Delegate تشغيل نماذجك (جزئيًا أو كليًا) على منفذ آخر. يمكن لهذه الآلية الاستفادة من مجموعة متنوعة من المسرعات الموجودة على الجهاز مثل GPU أو Edge TPU (وحدة معالجة Tensor) للاستدلال. وهذا يوفر للمطورين طريقة مرنة ومنفصلة عن TFLite الافتراضي لتسريع الاستدلال.

الرسم البياني أدناه يلخص المندوبين، مزيد من التفاصيل في الأقسام أدناه.

TFLite Delegates

متى يجب علي إنشاء مفوض مخصص؟

يحتوي TensorFlow Lite على مجموعة واسعة من المفوضين للمسرعات المستهدفة مثل GPU وDSP وEdgeTPU وأطر العمل مثل Android NNAPI.

يعد إنشاء المفوض الخاص بك مفيدًا في السيناريوهات التالية:

  • تريد دمج محرك استدلال ML جديد غير مدعوم من قبل أي مفوض موجود.
  • لديك مُسرّع أجهزة مخصص يعمل على تحسين وقت التشغيل للسيناريوهات المعروفة.
  • أنت تعمل على تطوير تحسينات لوحدة المعالجة المركزية (مثل دمج المشغل) التي يمكنها تسريع نماذج معينة.

كيف يعمل المندوبون؟

ضع في اعتبارك رسمًا بيانيًا نموذجيًا بسيطًا مثل ما يلي، والمفوض "MyDelegate" الذي يتمتع بتنفيذ أسرع لعمليات Conv2D وMean.

Original graph

بعد تطبيق "MyDelegate"، سيتم تحديث الرسم البياني الأصلي لـ TensorFlow Lite كما يلي:

Graph with delegate

يتم الحصول على الرسم البياني أعلاه عندما يقوم TensorFlow Lite بتقسيم الرسم البياني الأصلي باتباع قاعدتين:

  • يتم وضع العمليات المحددة التي يمكن للمفوض التعامل معها في قسم مع الاستمرار في تلبية تبعيات سير عمل الحوسبة الأصلية بين العمليات.
  • يحتوي كل قسم سيتم تفويضه على عقد الإدخال والإخراج التي لا تتم معالجتها بواسطة المفوض.

يتم استبدال كل قسم تتم معالجته بواسطة المفوض بعقدة المفوض (يمكن استدعاؤها أيضًا كنواة المفوض) في الرسم البياني الأصلي الذي يقيم القسم عند استدعاء الاستدعاء الخاص به.

اعتمادًا على النموذج، يمكن أن ينتهي الرسم البياني النهائي بعقدة واحدة أو أكثر، ويعني الأخير أن بعض العمليات غير مدعومة من قبل المفوض. بشكل عام، لا ترغب في معالجة أقسام متعددة بواسطة المفوض، لأنه في كل مرة تقوم فيها بالتبديل من المفوض إلى الرسم البياني الرئيسي، يكون هناك حمل لتمرير النتائج من الرسم البياني الفرعي المفوض إلى الرسم البياني الرئيسي الذي ينتج بسبب الذاكرة نسخ (على سبيل المثال، GPU إلى وحدة المعالجة المركزية). قد يعوض مثل هذا الحمل مكاسب الأداء خاصة عند وجود كمية كبيرة من نسخ الذاكرة.

تنفيذ المفوض المخصص الخاص بك

الطريقة المفضلة لإضافة مفوض هي استخدام SimpleDelegate API .

لإنشاء مفوض جديد، تحتاج إلى تنفيذ واجهتين وتوفير التنفيذ الخاص بك لطرق الواجهة.

1 - SimpleDelegateInterface

تمثل هذه الفئة قدرات المفوض، والعمليات المدعومة، وفئة المصنع لإنشاء نواة تحتوي على الرسم البياني المفوض. لمزيد من التفاصيل، راجع الواجهة المحددة في ملف رأس C++ هذا. تشرح التعليقات الموجودة في الكود كل واجهة برمجة تطبيقات بالتفصيل.

2 - SimpleDelegateKernelInterface

تحتوي هذه الفئة على منطق تهيئة/تحضير/وتشغيل القسم المفوض.

لديها: (انظر التعريف )

  • Init(...): والذي سيتم استدعاؤه مرة واحدة للقيام بأي تهيئة لمرة واحدة.
  • تحضير (...): يتم استدعاؤه لكل مثيل مختلف لهذه العقدة - يحدث هذا إذا كان لديك أقسام مفوضة متعددة. عادةً ما تريد إجراء عمليات تخصيص للذاكرة هنا، حيث سيتم استدعاء ذلك في كل مرة يتم فيها تغيير حجم الموترات.
  • استدعاء (...): والذي سيتم استدعاؤه للاستدلال.

مثال

في هذا المثال، ستقوم بإنشاء مندوب بسيط للغاية يمكنه دعم نوعين فقط من العمليات (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، أحدهما يشغل نواة وحدة المعالجة المركزية ذات الخيوط الواحدة والآخر يستخدم مواصفات محددة من قبل المستخدم. فهو يقيس الفرق المطلق بين موترات الإخراج من كل مترجم، على أساس كل عنصر. يمكن أن تكون هذه الأداة مفيدة أيضًا في تصحيح مشكلات الدقة.
  • هناك أيضًا أدوات تقييم محددة للمهام، لتصنيف الصور واكتشاف الكائنات. يمكن العثور على هذه الأدوات هنا

بالإضافة إلى ذلك، يحتوي TFLite على مجموعة كبيرة من اختبارات وحدة النواة والعمليات التي يمكن إعادة استخدامها لاختبار المندوب الجديد بتغطية أكبر ولضمان عدم انقطاع مسار تنفيذ TFLite العادي.

لتحقيق إعادة استخدام اختبارات وأدوات TFLite للمفوض الجديد، يمكنك استخدام أي من الخيارين التاليين:

اختيار النهج الأفضل

يتطلب كلا الأسلوبين بعض التغييرات كما هو مفصل أدناه. ومع ذلك، فإن النهج الأول يربط المندوب بشكل ثابت ويتطلب إعادة بناء أدوات الاختبار والقياس والتقييم. في المقابل، فإن الخيار الثاني يجعل المفوض كمكتبة مشتركة ويتطلب منك الكشف عن طرق الإنشاء/الحذف من المكتبة المشتركة.

ونتيجة لذلك، ستعمل آلية التفويض الخارجي مع ثنائيات أدوات Tensorflow Lite المعدة مسبقًا من TFLite. لكنها أقل وضوحًا وقد يكون إعدادها في اختبارات التكامل الآلية أكثر تعقيدًا. استخدم نهج المسجل المفوض للحصول على وضوح أفضل.

الخيار 1: الاستفادة من المسجل المفوض

يحتفظ مسجل المفوضين بقائمة موفري المفوضين، كل منهم يوفر طريقة سهلة لإنشاء مفوضي TFLite استنادًا إلى علامات سطر الأوامر، وبالتالي، يكون مناسبًا للأدوات. لتوصيل المفوض الجديد بجميع أدوات Tensorflow Lite المذكورة أعلاه، عليك أولاً إنشاء موفر مفوض جديد مثل هذا ، ثم إجراء بعض التغييرات فقط على قواعد BUILD. يظهر أدناه مثال كامل لعملية التكامل هذه (ويمكن العثور على الكود هنا ).

بافتراض أن لديك مفوضًا يقوم بتنفيذ واجهات برمجة تطبيقات SimpleDelegate، وواجهات برمجة التطبيقات "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.
)

أضف الآن هاتين القاعدتين المجمعتين في ملف 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 kernel كما هو موضح هنا .

الخيار 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++ المقابل للمفوض في ربط Tensorflow Lite Python كما هو موضح هنا . لذلك، يمكن استخدام مكتبة محول المفوض الخارجي الديناميكي التي تم إنشاؤها هنا مباشرةً مع Tensorflow Lite Python APIs.

موارد

نظام التشغيل قوس BINARY_NAME
لينكس x86_64
ذراع
aarch64
ذكري المظهر ذراع
aarch64