پیاده سازی یک نماینده سفارشی

LiteRT Delegate به شما این امکان را می‌دهد که مدل‌های خود را (قسمتی یا کامل) روی یک اجراکننده دیگر اجرا کنید. این مکانیسم می تواند از انواع شتاب دهنده های روی دستگاه مانند GPU یا Edge TPU (واحد پردازش تانسور) برای استنتاج استفاده کند. این به توسعه دهندگان یک روش منعطف و جدا شده از TFLite پیش فرض برای سرعت بخشیدن به استنتاج ارائه می دهد.

نمودار زیر نمایندگان را خلاصه می کند، جزئیات بیشتر در بخش های زیر.

TFLite Delegates

چه زمانی باید یک نماینده سفارشی ایجاد کنم؟

LiteRT دارای طیف گسترده ای از نمایندگان برای شتاب دهنده های هدف مانند GPU، DSP و EdgeTPU است.

ایجاد نماینده خود در سناریوهای زیر مفید است:

  • شما می خواهید یک موتور استنتاج ML جدید را ادغام کنید که توسط هیچ نماینده موجود پشتیبانی نمی شود.
  • شما یک شتاب دهنده سخت افزاری سفارشی دارید که زمان اجرا را برای سناریوهای شناخته شده بهبود می بخشد.
  • شما در حال توسعه بهینه سازی های CPU (مانند فیوزینگ اپراتور) هستید که می تواند سرعت مدل های خاصی را افزایش دهد.

نمایندگان چگونه کار می کنند؟

یک نمودار مدل ساده مانند شکل زیر و یک نماینده "MyDelegate" را در نظر بگیرید که اجرای سریع تری برای عملیات Conv2D و Mean دارد.

Original graph

پس از اعمال این "MyDelegate"، نمودار LiteRT اصلی مانند زیر به روز می شود:

Graph with delegate

نمودار بالا زمانی به دست می آید که LiteRT نمودار اصلی را زیر دو قانون تقسیم می کند:

  • عملیات خاصی که می‌تواند توسط نماینده انجام شود، در یک پارتیشن قرار می‌گیرند و در عین حال وابستگی‌های اولیه گردش کار محاسباتی را در بین عملیات برآورده می‌کنند.
  • هر پارتیشن قابل واگذاری فقط دارای گره های ورودی و خروجی است که توسط نماینده مدیریت نمی شود.

هر پارتیشنی که توسط یک نماینده اداره می شود با یک گره نماینده (همچنین می تواند به عنوان هسته نماینده فراخوانی شود) در گراف اصلی جایگزین می شود که پارتیشن را در فراخوانی فراخوانی آن ارزیابی می کند.

بسته به مدل، نمودار نهایی می تواند به یک یا چند گره ختم شود، که دومی به این معنی است که برخی از عملیات ها توسط نماینده پشتیبانی نمی شوند. به طور کلی، شما نمی خواهید چندین پارتیشن توسط نماینده مدیریت شود، زیرا هر بار که از نماینده به گراف اصلی جابجا می شوید، هزینه ای برای انتقال نتایج از زیرگراف واگذار شده به گراف اصلی وجود دارد که ناشی از حافظه است. کپی (به عنوان مثال، GPU به CPU). چنین سرباری ممکن است افزایش عملکرد را جبران کند، به خصوص زمانی که مقدار زیادی نسخه حافظه وجود دارد.

پیاده سازی نماینده سفارشی خود

روش ترجیحی برای افزودن نماینده استفاده از SimpleDelegate API است.

برای ایجاد یک نماینده جدید، باید 2 رابط را پیاده سازی کنید و پیاده سازی خود را برای روش های رابط ارائه دهید.

1 - SimpleDelegateInterface

این کلاس نشان دهنده قابلیت های نماینده است که از عملیات پشتیبانی می شود و یک کلاس کارخانه برای ایجاد یک هسته که گراف واگذار شده را محصور می کند. برای جزئیات بیشتر، رابط تعریف شده در این فایل هدر C++ را ببینید. نظرات موجود در کد هر API را با جزئیات توضیح می دهد.

2 - SimpleDelegateKernelInterface

این کلاس منطق را برای مقداردهی اولیه / آماده سازی / و اجرای پارتیشن واگذار شده محصور می کند.

دارای: (به تعریف مراجعه کنید)

  • Init(...): که یک بار برای انجام هر مقدار اولیه یک بار فراخوانی می شود.
  • Prepare(...): برای هر نمونه متفاوت از این گره فراخوانی می شود - اگر چندین پارتیشن تفویض شده داشته باشید این اتفاق می افتد. معمولاً می‌خواهید تخصیص حافظه را در اینجا انجام دهید، زیرا هر بار که اندازه تانسورها تغییر می‌کنند، این نام خوانده می‌شود.
  • Invoke(...): که برای استنتاج فراخوانده می شود.

مثال

در این مثال، یک نماینده بسیار ساده ایجاد می کنید که می تواند تنها 2 نوع عملیات (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 می گیرد، ورودی های تصادفی تولید می کند و سپس مدل را برای تعداد مشخصی اجرا به طور مکرر اجرا می کند. آمار تأخیر جمعی را در پایان چاپ می کند.
  • Inference Diff Tool : برای یک مدل معین، این ابزار داده‌های تصادفی گاوسی را تولید می‌کند و آن را از طریق دو مفسر TFLite مختلف منتقل می‌کند، یکی از هسته‌های CPU تک رشته‌ای و دیگری با استفاده از مشخصات تعریف‌شده توسط کاربر. تفاوت مطلق بین تانسورهای خروجی هر مفسر را بر اساس هر عنصر اندازه گیری می کند. این ابزار همچنین می تواند برای اشکال زدایی مشکلات دقت مفید باشد.
  • همچنین ابزارهای ارزیابی ویژه ای برای طبقه بندی تصویر و تشخیص اشیا وجود دارد. این ابزارها را می توانید در اینجا پیدا کنید

علاوه بر این، TFLite مجموعه بزرگی از آزمایش‌های هسته و واحد عملیاتی دارد که می‌توانند برای آزمایش نماینده جدید با پوشش بیشتر و اطمینان از شکسته نشدن مسیر اجرای معمولی TFLite دوباره استفاده شوند.

برای دستیابی به استفاده مجدد از تست ها و ابزار TFLite برای نماینده جدید، می توانید از یکی از دو گزینه زیر استفاده کنید:

انتخاب بهترین رویکرد

هر دو رویکرد به چند تغییر نیاز دارند که در زیر توضیح داده شده است. با این حال، رویکرد اول نماینده را به صورت ایستا پیوند می‌دهد و نیازمند بازسازی ابزارهای آزمایش، معیار و ارزیابی است. در مقابل، مورد دوم نماینده را به عنوان یک کتابخانه مشترک تبدیل می‌کند و از شما می‌خواهد که روش‌های ایجاد/حذف از کتابخانه مشترک را در معرض نمایش بگذارید.

در نتیجه، مکانیسم نمایندگی خارجی با باینری‌های ابزار از پیش ساخته شده LiteRT TFLite کار می‌کند. اما این کمتر صریح است و ممکن است تنظیم آن در تست های یکپارچه سازی خودکار پیچیده تر باشد. برای وضوح بهتر از رویکرد ثبت نماینده استفاده کنید.

گزینه 1: از ثبت کننده نمایندگی استفاده کنید

ثبت کننده نماینده لیستی از ارائه دهندگان نمایندگی را نگه می دارد، که هر یک از آنها راه آسانی برای ایجاد نمایندگان TFLite بر اساس پرچم های خط فرمان ارائه می دهد و از این رو برای ابزارسازی راحت هستند. برای وصل کردن نماینده جدید به تمام ابزارهای LiteRT که در بالا ذکر شد، ابتدا یک ارائه دهنده نماینده جدید ایجاد می کنید و سپس تنها چند تغییر در قوانین 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.
)

اکنون این دو قانون پوشش را در فایل 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",
    ],
)

پس از ایجاد این فایل delegate .so خارجی، می‌توانید باینری بسازید یا از موارد از پیش ساخته شده برای اجرا با نماینده جدید استفاده کنید، تا زمانی که باینری با کتابخانه external_delegate_provider مرتبط است که از پرچم‌های خط فرمان همانطور که در اینجا توضیح داده شده پشتیبانی می‌کند. توجه: این ارائه‌دهنده نمایندگی خارجی قبلاً به باینری‌های آزمایشی و ابزارسازی موجود مرتبط شده است.

برای توضیح نحوه محک زدن نماینده ساختگی از طریق این رویکرد نمایندگی خارجی، به توضیحات اینجا مراجعه کنید. می توانید از دستورات مشابه برای ابزارهای تست و ارزیابی که قبلا ذکر شد استفاده کنید.

شایان ذکر است که نماینده خارجی همان پیاده سازی C++ مربوط به نماینده در LiteRT Python binding است که در اینجا نشان داده شده است. بنابراین، کتابخانه آداپتور نماینده خارجی پویا ایجاد شده در اینجا می‌تواند مستقیماً با APIهای LiteRT Python استفاده شود.

منابع

سیستم عامل ARCH BINARY_NAME
لینوکس x86_64
بازو
aarch64
اندروید بازو
aarch64