הפעלת LiteRT Next ב-Android באמצעות C++‎

ממשקי ה-API של LiteRT Next זמינים ב-C++‎, והם יכולים להציע למפתחי Android שליטה רבה יותר על הקצאת הזיכרון ועל פיתוח ברמה נמוכה יותר מאשר ממשקי ה-API של Kotlin.

דוגמה לאפליקציית LiteRT Next ב-C++‎ זמינה בדמו של פילוח אסינכרוני ב-C++‎.

שנתחיל?

כדי להוסיף את LiteRT Next לאפליקציה ל-Android:

עדכון תצורת ה-build

כדי ליצור אפליקציה ב-C++ עם LiteRT להאצת GPU, NPU ו-CPU באמצעות Bazel, צריך להגדיר כלל cc_binary כדי לוודא שכל הרכיבים הנדרשים מקובצים, מקושרים ומתומללים. ההגדרה לדוגמה הבאה מאפשרת לאפליקציה לבחור באופן דינמי במאיצי GPU, NPU ו-CPU או להשתמש בהם.

אלה הרכיבים העיקריים בהגדרת ה-build של 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).
    • ספריות Dispatch: ספריות משותפות ספציפיות לספק, שבהן LiteRT משתמש כדי לתקשר עם מנהלי ההתקנים של החומרה (למשל, //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • ספריות לקצה העורפי של GPU: הספריות המשותפות להאצת GPU (למשל, "@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).
    • קבצים ונכסים של מודלים: קובצי המודלים המאומנים, תמונות הבדיקה, השכבות לשיפור איכות התמונה או כל נתון אחר שנחוץ בסביבת זמן הריצה (למשל, :model_files, :shader_files).
  • מאפיין deps (יחסי תלות בזמן הידור): כאן מפורטות הספריות שצריך להיעזר בהן כדי להדר את הקוד.
    • LiteRT APIs & Utilities: כותרות וספריות סטטיות לרכיבי LiteRT כמו מאגרי טינסורים (למשל, //litert/cc:litert_tensor_buffer).
    • ספריות גרפיקה (ל-GPU): יחסי תלות שקשורים לממשקי API גרפיים, אם המאיץ של ה-GPU משתמש בהם (למשל, gles_deps()).
  • המאפיין linkopts: מציין אפשרויות שמועברות למקשר, שיכולות לכלול קישור לספריות מערכת (למשל, -landroid לגרסאות build של 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 מספק סביבה בסביבת זמן ריצה שכוללת רכיבים כמו הנתיב של הפלאגין של המהדר וקונטקסטים של GPU. השדה Environment נדרש כשיוצרים את השדות CompiledModel ו-TensorBuffer. הקוד הבא יוצר Environment להרצה ב-CPU וב-GPU ללא אפשרויות:

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

יצירת המודל המהדר

באמצעות ה-API של 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

מושגים ורכיבים מרכזיים

בקטעים הבאים מוסבר על רכיבים ומונחים מרכזיים של ממשקי ה-API של 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) ומאיץ החומרה היעד (GPU), ויוצר מודל הידור שעומד בקריטריונים להרצת היסק.

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

כדי לקבל תמונה מלאה יותר של אופן ההטמעה של ה-API של CompiledModel, אפשר לעיין בקוד המקור של litert_compiled_model.h.

מאגר נתונים זמני של טינסורים (TensorBuffer)

ב-LiteRT Next יש תמיכה מובנית בתאימות הדדית של מאגרי נתונים להעברת נתונים (I/O), באמצעות API של מאגר נתונים מסוג Tensor (TensorBuffer) לטיפול בזרימת הנתונים אל המודל המהדרג וממנו. באמצעות Tensor Buffer API אפשר לכתוב (Write<T>()) ולקרוא (Read<T>()) ולנעול את זיכרון המעבד.

כדי לקבל תמונה מלאה יותר של אופן ההטמעה של ממשק ה-API TensorBuffer, אפשר לעיין בקוד המקור של litert_tensor_buffer.h.

הדרישות של קלט/פלט של מודל השאילתה

הדרישות להקצאת מאגר טינסורים (TensorBuffer) בדרך כלל מצוינות על ידי המאיץ החומרתי. למאגרים של קלט ופלט יכולות להיות דרישות לגבי התאמה, צעדים של מאגר וסוג זיכרון. אפשר להשתמש בפונקציות עזר כמו CreateInputBuffers כדי לטפל בדרישות האלה באופן אוטומטי.

קטע הקוד הפשוט הבא מראה איך אפשר לאחזר את דרישות המאגר לנתוני הקלט:

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

כדי לקבל תמונה מלאה יותר של ההטמעה של ממשק ה-API TensorBufferRequirements, אפשר לעיין בקוד המקור של litert_tensor_buffer_requirements.h.

יצירת מאגרי טינסורים מנוהלים (TensorBuffers)

קטע הקוד הפשוט הבא מדגים איך יוצרים מאגרי נתונים מנוהלים של טיינורים, שבהם ה-API של 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));

יצירת מאגרי טינסורים ללא העתקה (zero-copy)

כדי לעטוף מאגר קיים כ-Tensor Buffer (zero-copy), משתמשים בקטע הקוד הבא:

// 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 Buffer

קטע הקוד הבא מדגים איך אפשר לקרוא ממאגר קלט ולכתוב למאגר פלט:

// 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 */
}

מתקדם: יכולת פעולה הדדית של מאגרי נתונים ללא העתקה (zero-copy) לסוגים מיוחדים של מאגרי נתונים בחומרה

סוגי מאגרים מסוימים, כמו 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++‎.

Basic Inference (מעבד)

בהמשך מוצגת גרסה מרוכזת של קטעי הקוד מהקטע תחילת העבודה. זוהי ההטמעה הפשוטה ביותר של היסק באמצעות 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));

העברה ללא העתקה (zero-copy) באמצעות זיכרון המארח

LiteRT Next Compiled Model API מפחית את החיכוך בצינורות עיבוד נתונים של מודלים של עיבוד, במיוחד כשעובדים עם כמה קצוות עורפיים של חומרה ותהליכי העברה ללא העתקה (zero-copy). קטע הקוד הבא משתמש בשיטה CreateFromHostMemory כשיוצרים את מאגר הקלט, שמשתמש ב-zero-copy עם זיכרון המארח.

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