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

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

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

שנתחיל?

כדי להוסיף את LiteRT Compiled Model API לאפליקציית 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: ספריית ה-API המשותפת הראשית של LiteRT C (לדוגמה, //litert/c:litert_runtime_c_api_shared_lib).
    • ספריות שליחה: ספריות משותפות ספציפיות לספק ש-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 מאפיין: מציין אפשרויות שמועברות ל-linker, שיכולות לכלול קישור לספריות מערכת (למשל, ‫-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 מספק סביבת זמן ריצה שכוללת רכיבים כמו הנתיב של תוסף הקומפיילר והקשרים של ה-GPU. העמודה Environment נדרשת כשיוצרים CompiledModel ו-TensorBuffer. הקוד הבא יוצר Environment להרצה ב-CPU וב-GPU ללא אפשרויות:

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

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

באמצעות CompiledModel API, מאתחלים את זמן הריצה עם אובייקט 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.

טיפול בשגיאות

הפונקציה litert::Expected ב-LiteRT מחזירה ערכים או מעבירה שגיאות בדומה לפונקציות 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)

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

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

Tensor Buffer (TensorBuffer)

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

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

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

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

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

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

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

יצירה של מאגרי Tensor מנוהלים (TensorBuffers)

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

יצירת Tensor Buffers ללא העתקה

כדי לעטוף מאגר קיים כמאגר טנסור (העתקה מאפס), משתמשים בקטע הקוד הבא:

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

מתקדם: פעולה הדדית של מאגרים ללא העתקה לסוגים מיוחדים של מאגרי חומרה

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

הסקת מסקנות בסיסית (CPU)

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

העתקה אפסית עם זיכרון המארח

ממשק ה-API של 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);