LiteRT CompiledModel API זמין ב-C++, ומאפשר למפתחים שליטה מדויקת בהקצאת זיכרון ופיתוח ברמה נמוכה. דוגמה מופיעה במאמר Image segmentation C++ App.
במדריך הבא מוצגת מסקנת CPU בסיסית של Kotlin API CompiledModel. במדריך בנושא האצת GPU והאצת NPU מוסבר על תכונות מתקדמות של האצה.
הוספת תלות ב-build
בוחרים את הנתיב שמתאים לפרויקט:
שימוש בספרייה מוכנה מראש (Android): אפשר להשתמש בספרייה מוכנה מראש מחבילת LiteRT Maven כדי להגדיר את התכונה באופן מיידי. איך משתמשים בספריית C++ מוכנה מראש מ-LiteRT Maven
קומפילציה ממקור (פלטפורמות שונות): קומפילציה ממקור לשליטה מלאה ותמיכה בפלטפורמות שונות (Android, iOS, macOS, Linux, Windows). ההוראות מפורטות בהמשך.
בנייה ממקור באמצעות Bazel
כדי ליצור אפליקציית 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).
- LiteRT Core Runtime: ספריית ה-API המשותפת הראשית של LiteRT C (לדוגמה,
depsמאפיין (יחסי תלות בזמן קומפילציה): כאן מפורטות הספריות שהקוד צריך להסתמך עליהן כדי לעבור קומפילציה.- LiteRT & Utilities: כותרות וספריות סטטיות לרכיבי LiteRT כמו מאגרי טנסורים (לדוגמה,
//litert/cc:litert_tensor_buffer). - ספריות גרפיקה (ל-GPU): תלויות שקשורות ל-API של גרפיקה אם נעשה בהן שימוש במאיץ ה-GPU (לדוגמה,
gles_deps()).
- LiteRT & Utilities: כותרות וספריות סטטיות לרכיבי LiteRT כמו מאגרי טנסורים (לדוגמה,
linkoptsמאפיין: מציין אפשרויות שמועברות ל-linker, שיכולות לכלול קישור לספריות מערכת (למשל, -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({}));
יצירת CompiledModel
באמצעות 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
מושגים ורכיבים מרכזיים
בקטעים הבאים מוסבר על מושגים ורכיבים מרכזיים של LiteRT CompiledModel API.
טיפול בשגיאות
הפונקציה 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.
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 CompiledModel מפחית את החיכוך בצינורות של הסקת מסקנות, במיוחד כשמדובר במספר קצוות עורפיים של חומרה ובזרימות של העתקה אפסית. בקטע הקוד הבא נעשה שימוש בשיטה 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);