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

تتوفّر واجهات برمجة التطبيقات LiteRT Next بتنسيق C++، ويمكنها أن توفّر لمطوّري تطبيقات Android إمكانية تحكّم أكبر في تخصيص الذاكرة والتطوير على مستوى منخفض مقارنةً بواجهات برمجة التطبيقات Kotlin.

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

البدء

اتّبِع الخطوات التالية لإضافة LiteRT Next إلى تطبيق Android.

تعديل إعدادات الإنشاء

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

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

  • cc_binary القاعدة: هذه هي قاعدة Bazel الأساسية المستخدَمة لتحديدهدف قابل للتنفيذ بلغة C++ (مثل name = "your_application_name").
  • srcs السمة: تسرد ملفات C++ المصدر لتطبيقك (مثل ‫main.cc وملفات .cc أو .h أخرى)
  • data السمة (التبعيات في وقت التشغيل): هذه السمة مهمة لحزمة المكتبات المشتركة ومواد العرض التي يحمّلها تطبيقك في وقت التشغيل.
    • LiteRT Core Runtime: المكتبة المشتركة الرئيسية لواجهة برمجة التطبيقات LiteRT C API (مثل //litert/c:litert_runtime_c_api_shared_lib).
    • مكتبات الإرسال: هي مكتبات مشتركة خاصة بالمورّد يستخدمها LiteRT للتواصل مع برامج تشغيل الأجهزة (مثل //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • مكتبات الخلفية لوحدة معالجة الرسومات: المكتبات المشتركة لتسريع وحدة معالجة الرسومات (مثل "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • مكتبات NPU الخلفية: المكتبات المشتركة المحدّدة لتسريع NPU، مثل مكتبات QNN HTP من Qualcomm (مثل @qairt//:lib/aarch64-android/libQnnHtp.so، @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • ملفات النماذج ومواد العرض: هي ملفات النماذج المدربة أو الصور الاختبارية أوshaders أو أي بيانات أخرى مطلوبة أثناء التشغيل (مثل :model_files، :shader_files).
  • سمة deps (العناصر الاعتمادية في وقت الترجمة): تعرض هذه السمة المكتبات التي يحتاج رمزك إلى الترجمة وفقًا لها.
    • LiteRT APIs & Utilities: تشمل الرؤوس والمكتبات الثابتة لمكوّنات 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 Next.

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

يستخدم 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 Next إمكانات مدمجة للتشغيل التفاعلي بين مخزنَي الإدخال/الإخراج، وذلك باستخدام واجهة برمجة التطبيقات Tensor Buffer API (TensorBuffer) لمعالجة تدفق البيانات إلى النموذج المجمّع ومنه. توفّر Tensor Buffer API إمكانية الكتابة (Write<T>()) والقراءة (Read<T>()) وقفل ذاكرة وحدة المعالجة المركزية.

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

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

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

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

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

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

إنشاء Buffers Tensor مُدارة (TensorBuffers)

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

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));

إنشاء Buffers Tensor باستخدام ميزة "النسخ بدون ذاكرة تخزين مؤقت"

لتغليف مخزن مؤقت حالي كـ "مخزن مؤقت للعناصر المصغّرة" (بدون نسخ)، استخدِم مقتطف الرمز التالي:

// 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));

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

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

// 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 Next في C++.

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

في ما يلي نسخة مكثّفة من مقتطفات الرموز من قسم بدء استخدام. وهو أبسط تطبيق للاستنتاج باستخدام LiteRT Next.

// 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));

ميزة "النسخ بدون تخزين مؤقت" مع ذاكرة المضيف

تعمل LiteRT Next Compiled Model API على تقليل المشاكل في قنوات الاستنتاج، خاصةً عند التعامل مع الخلفيات المتعدّدة للأجهزة ومسارات نقل البيانات بدون نسخ. يستخدم مقتطف الرمز البرمجي التالي الأسلوب 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);