تشغيل LiteRT Compiled Model API على Android باستخدام C++

تتوفّر واجهات برمجة التطبيقات الخاصة بنماذج LiteRT المجمَّعة بلغة C++، ما يتيح لمطوّري تطبيقات Android التحكّم بدقة في عملية تخصيص الذاكرة والتطوير على مستوى منخفض.

للاطّلاع على مثال لتطبيق LiteRT بلغة C++‎، راجِع عرض توضيحي حول التقسيم غير المتزامن باستخدام C++‎.

البدء

اتّبِع الخطوات التالية لإضافة واجهة برمجة التطبيقات LiteRT Compiled Model API إلى تطبيق Android.

تعديل إعدادات الإصدار

يتضمّن إنشاء تطبيق C++‎ باستخدام LiteRT لتسريع وحدة معالجة الرسومات ووحدة المعالجة العصبية ووحدة المعالجة المركزية باستخدام Bazel تحديد قاعدة cc_binary لضمان تجميع جميع المكوّنات الضرورية وربطها وتعبئتها. يسمح مثال الإعداد التالي لتطبيقك باختيار مسرّعات وحدة معالجة الرسومات ووحدة المعالجة العصبية ووحدة المعالجة المركزية أو استخدامها بشكل ديناميكي.

في ما يلي المكوّنات الرئيسية في إعدادات الإصدار في Bazel:

  • cc_binary القاعدة: هذه هي قاعدة Bazel الأساسية المستخدَمة لتحديد هدفك القابل للتنفيذ بلغة C++ (على سبيل المثال، name = "your_application_name").
  • srcs السمة: تسرد ملفات المصدر C++ الخاصة بتطبيقك (مثل ‫main.cc، وملفات .cc أو .h أخرى).
  • السمة data (التبعيات في وقت التشغيل): هذه السمة ضرورية لتضمين المكتبات المشترَكة ومواد العرض التي يحمّلها تطبيقك في وقت التشغيل.
    • وقت التشغيل الأساسي في LiteRT: مكتبة C API الرئيسية المشترَكة في LiteRT (مثل //litert/c:litert_runtime_c_api_shared_lib).
    • مكتبات الإرسال: هي مكتبات مشتركة خاصة بمورّدين معيّنين تستخدمها LiteRT للتواصل مع برامج تشغيل الأجهزة (مثل //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • مكتبات الخلفية لوحدة معالجة الرسومات: هي المكتبات المشتركة لتسريع وحدة معالجة الرسومات (مثل "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • مكتبات الخلفية لوحدة المعالجة العصبية: هي المكتبات المشتركة الخاصة بتسريع وحدة المعالجة العصبية، مثل مكتبات QNN HTP من Qualcomm (مثل @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • ملفات النماذج ومواد العرض: ملفات النماذج المدرَّبة أو صور الاختبار أو برامج التظليل أو أي بيانات أخرى مطلوبة أثناء وقت التشغيل (مثل :model_files, :shader_files).
  • السمة deps (العناصر التابعة في وقت الترجمة): تعرض هذه السمة المكتبات التي يحتاجها الرمز البرمجي الخاص بك ليتم تجميعه.
    • واجهات برمجة التطبيقات والأدوات المساعدة في LiteRT: تتضمّن هذه الحزمة العناوين والمكتبات الثابتة لمكوّنات LiteRT، مثل مخازن موترات (مثل //litert/cc:litert_tensor_buffer).
    • مكتبات الرسومات (لوحدة معالجة الرسومات): العناصر التابعة المرتبطة بواجهات برمجة تطبيقات الرسومات إذا كان مسرِّع وحدة معالجة الرسومات يستخدمها (مثل gles_deps()).
  • السمة linkopts: تحدّد الخيارات التي يتم تمريرها إلى أداة الربط، والتي يمكن أن تتضمّن الربط بمكتبات النظام (مثل -landroid لإنشاء تطبيقات Android، أو مكتبات GLES التي تتضمّن gles_linkopts()).

في ما يلي مثال على قاعدة cc_binary:

cc_binary(
    name = "your_application",
    srcs = [
        "main.cc",
    ],
    data = [
        ...
        # litert c api shared library
        "//litert/c:litert_runtime_c_api_shared_lib",
        # GPU accelerator shared library
        "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so",
        # NPU accelerator shared library
        "//litert/vendors/qualcomm/dispatch:dispatch_api_so",
    ],
    linkopts = select({
        "@org_tensorflow//tensorflow:android": ["-landroid"],
        "//conditions:default": [],
    }) + gles_linkopts(), # gles link options
    deps = [
        ...
        "//litert/cc:litert_tensor_buffer", # litert cc library
        ...
    ] + gles_deps(), # gles dependencies
)

تحميل النموذج

بعد الحصول على نموذج LiteRT أو تحويل نموذج إلى تنسيق .tflite، حمِّل النموذج عن طريق إنشاء عنصر Model.

LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));

إنشاء البيئة

يوفر الكائن Environment بيئة وقت تشغيل تتضمّن مكوّنات، مثل مسار المكوّن الإضافي للمجمّع وسياقات وحدة معالجة الرسومات. السمة Environment مطلوبة عند إنشاء CompiledModel وTensorBuffer. ينشئ الرمز البرمجي التالي Environment لتنفيذ وحدة المعالجة المركزية ووحدة معالجة الرسومات بدون أي خيارات:

LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));

إنشاء النموذج المجمَّع

باستخدام واجهة برمجة التطبيقات CompiledModel، يمكنك إعداد وقت التشغيل باستخدام الكائن Model الذي تم إنشاؤه حديثًا. يمكنك تحديد تسريع الأجهزة في هذه المرحلة (kLiteRtHwAcceleratorCpu أو kLiteRtHwAcceleratorGpu):

LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, kLiteRtHwAcceleratorCpu));

إنشاء مخازن مؤقتة للإدخال والإخراج

أنشئ بُنى البيانات اللازمة (المخازن المؤقتة) لاحتواء بيانات الإدخال التي ستُدخلها إلى النموذج للاستدلال، وبيانات الإخراج التي ينتجها النموذج بعد تنفيذ الاستدلال.

LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

إذا كنت تستخدم ذاكرة وحدة المعالجة المركزية، املأ المدخلات بكتابة البيانات مباشرةً في مخزن الإدخال المؤقت الأول.

input_buffers[0].Write<float>(absl::MakeConstSpan(input_data, input_size));

استدعاء النموذج

قدِّم مخزّنَي البيانات المؤقتة للإدخال والإخراج، وشغِّل "النموذج المجمّع" مع النموذج وتسريع الأجهزة المحدّدَين في الخطوات السابقة.

compiled_model.Run(input_buffers, output_buffers);

استرداد النتائج

استرداد النتائج من خلال قراءة ناتج النموذج من الذاكرة مباشرةً

std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));
// ... process output data

المفاهيم والمكوّنات الرئيسية

راجِع الأقسام التالية للحصول على معلومات حول المفاهيم والمكوّنات الأساسية لواجهات برمجة التطبيقات الخاصة بنماذج LiteRT المجمَّعة.

خطأ أثناء المعالجة

تستخدم LiteRT الدالة litert::Expected لعرض القيم أو نشر الأخطاء بطريقة مشابهة للدالتَين absl::StatusOr أو std::expected. يمكنك التحقّق من الخطأ يدويًا.

لتسهيل الأمر، يوفّر LiteRT وحدات الماكرو التالية:

  • تُعيّن الدالة LITERT_ASSIGN_OR_RETURN(lhs, expr) نتيجة expr إلى lhs إذا لم ينتج عنها خطأ، وإلا فإنّها تعرض الخطأ.

    سيتم توسيعه ليصبح مشابهًا للمقتطف التالي.

    auto maybe_model = Model::CreateFromFile("mymodel.tflite");
    if (!maybe_model) {
      return maybe_model.Error();
    }
    auto model = std::move(maybe_model.Value());
    
  • تؤدي LITERT_ASSIGN_OR_ABORT(lhs, expr) الوظيفة نفسها التي تؤديها LITERT_ASSIGN_OR_RETURN، ولكنها توقف البرنامج في حال حدوث خطأ.

  • تعرض الدالة LITERT_RETURN_IF_ERROR(expr) القيمة expr إذا أدّى التقييم إلى حدوث خطأ.

  • تنفّذ LITERT_ABORT_IF_ERROR(expr) الإجراء نفسه الذي تنفّذه LITERT_RETURN_IF_ERROR، ولكنها توقف البرنامج في حال حدوث خطأ.

لمزيد من المعلومات حول وحدات ماكرو LiteRT، يُرجى الاطّلاع على litert_macros.h.

النموذج المجمَّع (CompiledModel)

تتولّى واجهة برمجة التطبيقات "النموذج المجمَّع" (Compiled Model API) (CompiledModel) مسؤولية تحميل نموذج وتطبيق تسريع الأجهزة وإنشاء وقت التشغيل وإنشاء مخازن مؤقتة للإدخال والإخراج وتشغيل الاستدلال.

يوضّح مقتطف الرمز المبسّط التالي كيف تأخذ واجهة برمجة التطبيقات Compiled Model API نموذج LiteRT (.tflite) ومسرِّع الأجهزة المستهدف (وحدة معالجة الرسومات)، وتنشئ نموذجًا مجمّعًا جاهزًا لتنفيذ الاستدلال.

// Load model and initialize runtime
LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, kLiteRtHwAcceleratorCpu));

يوضّح مقتطف الرمز المبسّط التالي كيف تأخذ واجهة برمجة التطبيقات Compiled Model API مخزنًا مؤقتًا للإدخال والإخراج، وتنفّذ عمليات الاستدلال باستخدام النموذج المجمَّع.

// Preallocate input/output buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// Fill the first input
float input_values[] = { /* your data */ };
LITERT_RETURN_IF_ERROR(
  input_buffers[0].Write<float>(absl::MakeConstSpan(input_values, /*size*/)));

// Invoke
LITERT_RETURN_IF_ERROR(compiled_model.Run(input_buffers, output_buffers));

// Read the output
std::vector<float> data(output_data_size);
LITERT_RETURN_IF_ERROR(
  output_buffers[0].Read<float>(absl::MakeSpan(data)));

للحصول على عرض أكثر اكتمالاً لطريقة تنفيذ واجهة برمجة التطبيقات CompiledModel، راجِع الرمز المصدري الخاص بـ litert_compiled_model.h.

مخزن مؤقت للمتّجه (TensorBuffer)

توفّر LiteRT إمكانية التشغيل التفاعلي لمخزن الإدخال/الإخراج المؤقت، وذلك باستخدام واجهة برمجة التطبيقات Tensor Buffer API (TensorBuffer) للتعامل مع تدفق البيانات من النموذج المترجَم وإليه. توفّر واجهة برمجة التطبيقات Tensor Buffer API إمكانية الكتابة (Write<T>()) والقراءة (Read<T>()) وتأمين ذاكرة وحدة المعالجة المركزية.

للحصول على عرض أكثر اكتمالاً لطريقة تنفيذ واجهة برمجة التطبيقات TensorBuffer، راجِع رمز المصدر الخاص بـ litert_tensor_buffer.h.

متطلبات إدخال/إخراج نموذج طلب البحث

عادةً ما يحدّد مسرّع الأجهزة متطلبات تخصيص مساحة تخزين مؤقت Tensor (TensorBuffer). يمكن أن تتضمّن مخازن مؤقتة للمدخلات والمخرجات متطلبات بشأن المحاذاة وخطوات المخزن المؤقت ونوع الذاكرة. يمكنك استخدام دوال مساعدة، مثل CreateInputBuffers، للتعامل مع هذه المتطلبات تلقائيًا.

يوضّح مقتطف الرمز المبسَّط التالي كيفية استرداد متطلبات المخزن المؤقت لبيانات الإدخال:

LITERT_ASSIGN_OR_RETURN(auto reqs, compiled_model.GetInputBufferRequirements(signature_index, input_index));

للحصول على عرض أكثر اكتمالاً لطريقة تنفيذ واجهة برمجة التطبيقات TensorBufferRequirements، يمكنك الاطّلاع على رمز المصدر الخاص بـ litert_tensor_buffer_requirements.h.

إنشاء مخازن مؤقتة مُدارة للموتّرات (TensorBuffers)

يوضّح مقتطف الرمز البرمجي المبسّط التالي كيفية إنشاء Managed Tensor Buffers، حيث يخصّص TensorBuffer API المخازن المؤقتة المعنية:

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_cpu,
TensorBuffer::CreateManaged(env, /*buffer_type=*/kLiteRtTensorBufferTypeHostMemory,
  ranked_tensor_type, buffer_size));

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_gl, TensorBuffer::CreateManaged(env,
  /*buffer_type=*/kLiteRtTensorBufferTypeGlBuffer, ranked_tensor_type, buffer_size));

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_ahwb, TensorBuffer::CreateManaged(env,
  /*buffer_type=*/kLiteRtTensorBufferTypeAhwb, ranked_tensor_type, buffer_size));

إنشاء مخازن Tensor Buffers باستخدام ميزة "عدم النسخ"

لتضمين مخزن مؤقت حالي كـ Tensor Buffer (بدون نسخ)، استخدِم مقتطف الرمز التالي:

// Create a TensorBuffer from host memory
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_host,
  TensorBuffer::CreateFromHostMemory(env, ranked_tensor_type,
  ptr_to_host_memory, buffer_size));

// Create a TensorBuffer from GlBuffer
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_gl,
  TensorBuffer::CreateFromGlBuffer(env, ranked_tensor_type, gl_target, gl_id,
  size_bytes, offset));

// Create a TensorBuffer from AHardware Buffer
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_ahwb,
  TensorBuffer::CreateFromAhwb(env, ranked_tensor_type, ahardware_buffer, offset));

القراءة والكتابة من "مخزن مؤقت للموتر"

يوضّح المقتطف التالي كيفية القراءة من مخزن مؤقت للإدخال والكتابة في مخزن مؤقت للإخراج:

// Example of reading to input buffer:
std::vector<float> input_tensor_data = {1,2};
LITERT_ASSIGN_OR_RETURN(auto write_success,
  input_tensor_buffer.Write<float>(absl::MakeConstSpan(input_tensor_data)));
if(write_success){
  /* Continue after successful write... */
}

// Example of writing to output buffer:
std::vector<float> data(total_elements);
LITERT_ASSIGN_OR_RETURN(auto read_success,
  output_tensor_buffer.Read<float>(absl::MakeSpan(data)));
if(read_success){
  /* Continue after successful read */
}

متقدّمة: إمكانية التشغيل التفاعلي لمخزن مؤقت بدون نسخ لأنواع المخازن المؤقتة للأجهزة المتخصّصة

تتيح بعض أنواع المخازن المؤقتة، مثل AHardwareBuffer، إمكانية التشغيل التفاعلي مع أنواع أخرى من المخازن المؤقتة. على سبيل المثال، يمكن إنشاء مخزن مؤقت OpenGL من AHardwareBuffer باستخدام ميزة "النسخ بدون وسيط". يعرض مقتطف الرمز التالي مثالاً:

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_ahwb,
  TensorBuffer::CreateManaged(env, kLiteRtTensorBufferTypeAhwb,
  ranked_tensor_type, buffer_size));
// Buffer interop: Get OpenGL buffer from AHWB,
// internally creating an OpenGL buffer backed by AHWB memory.
LITERT_ASSIGN_OR_RETURN(auto gl_buffer, tensor_buffer_ahwb.GetGlBuffer());

يمكن أيضًا إنشاء مخازن مؤقتة OpenCL من AHardwareBuffer:

LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_ahwb.GetOpenClMemory());

على الأجهزة الجوّالة التي تتيح إمكانية التشغيل التفاعلي بين OpenCL وOpenGL، يمكن إنشاء مخازن مؤقتة CL من مخازن مؤقتة GL:

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_gl,
  TensorBuffer::CreateFromGlBuffer(env, ranked_tensor_type, gl_target, gl_id,
  size_bytes, offset));

// Creates an OpenCL buffer from the OpenGL buffer, zero-copy.
LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_from_gl.GetOpenClMemory());

أمثلة على العناصر المنفّذة

راجِع عمليات تنفيذ LiteRT التالية في C++.

الاستدلال الأساسي (وحدة المعالجة المركزية)

في ما يلي نسخة مختصرة من مقتطفات الرموز البرمجية من قسم البدء. وهي أبسط طريقة لتنفيذ الاستدلال باستخدام LiteRT.

// Load model and initialize runtime
LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model, CompiledModel::Create(env, model,
  kLiteRtHwAcceleratorCpu));

// Preallocate input/output buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// Fill the first input
float input_values[] = { /* your data */ };
input_buffers[0].Write<float>(absl::MakeConstSpan(input_values, /*size*/));

// Invoke
compiled_model.Run(input_buffers, output_buffers);

// Read the output
std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));

Zero-Copy with Host Memory

تحدّ واجهة برمجة التطبيقات LiteRT Compiled Model من المشاكل التي تواجه خطوط نقل البيانات الخاصة بالاستدلال، خاصةً عند التعامل مع العديد من الأنظمة الخلفية للأجهزة وتدفقات النسخ الصفرية. يستخدم مقتطف الرمز التالي الطريقة CreateFromHostMemory عند إنشاء مخزن الإدخال المؤقت، الذي يستخدم النسخ بدون وسيط مع ذاكرة المضيف.

// Define an LiteRT environment to use existing EGL display and context.
const std::vector<Environment::Option> environment_options = {
   {OptionTag::EglDisplay, user_egl_display},
   {OptionTag::EglContext, user_egl_context}};
LITERT_ASSIGN_OR_RETURN(auto env,
   Environment::Create(absl::MakeConstSpan(environment_options)));

// Load model1 and initialize runtime.
LITERT_ASSIGN_OR_RETURN(auto model1, Model::CreateFromFile("model1.tflite"));
LITERT_ASSIGN_OR_RETURN(auto compiled_model1, CompiledModel::Create(env, model1, kLiteRtHwAcceleratorGpu));

// Prepare I/O buffers. opengl_buffer is given outside from the producer.
LITERT_ASSIGN_OR_RETURN(auto tensor_type, model.GetInputTensorType("input_name0"));
// Create an input TensorBuffer based on tensor_type that wraps the given OpenGL Buffer.
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_opengl,
    litert::TensorBuffer::CreateFromGlBuffer(env, tensor_type, opengl_buffer));

// Create an input event and attach it to the input buffer. Internally, it creates
// and inserts a fence sync object into the current EGL command queue.
LITERT_ASSIGN_OR_RETURN(auto input_event, Event::CreateManaged(env, LiteRtEventTypeEglSyncFence));
tensor_buffer_from_opengl.SetEvent(std::move(input_event));

std::vector<TensorBuffer> input_buffers;
input_buffers.push_back(std::move(tensor_buffer_from_opengl));

// Create an output TensorBuffer of the model1. It's also used as an input of the model2.
LITERT_ASSIGN_OR_RETURN(auto intermedidate_buffers,  compiled_model1.CreateOutputBuffers());

// Load model2 and initialize runtime.
LITERT_ASSIGN_OR_RETURN(auto model2, Model::CreateFromFile("model2.tflite"));
LITERT_ASSIGN_OR_RETURN(auto compiled_model2, CompiledModel::Create(env, model2, kLiteRtHwAcceleratorGpu));
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model2.CreateOutputBuffers());

compiled_model1.RunAsync(input_buffers, intermedidate_buffers);
compiled_model2.RunAsync(intermedidate_buffers, output_buffers);