تنفيذ تفويض مخصّص

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

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

مفوَّضو TFLite

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

يضم TensorFlow Lite مجموعة متنوعة من المستخدمين المفوَّضين بإدارة مسرِّعات الأعمال المستهدفة، مثل GPU وDSP وEdgeTPU.

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

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

ما هي طريقة عمل المفوَّضين؟

يمكنك استخدام رسم بياني بسيط للنموذج مثل ما يلي، واستخدام تفويض "MyDelegate" ينفّذ بشكلٍ أسرع لعمليات التنفيذ من أجل إجراء عمليّات "الإحالات الناجحة من نوع F2D" و"المتوسط"

الرسم البياني الأصلي

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

رسم بياني باستخدام المفوَّض

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

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

يتم استبدال كل قسم يعالجه المفوَّض بعقدة مفوَّضة (يمكن أن تُسمى أيضًا نواة التفويض) في الرسم البياني الأصلي الذي يقيّم القسم عند استدعائه.

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

تنفيذ المفوَّض المخصّص

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

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

من 1 إلى SimpleDelegateInterface

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

2 - SimpleDelegateKernelInterface

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

ويشتمل على: (التعريف)

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

مثال

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

الخيار 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 كما هو موضّح هنا.

الخيار 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.

المراجع

نظام التشغيل قوس BINARY_NAME
Linux x86_64
مجموعة التجربة
aarch64
Android مجموعة التجربة
aarch64