LiteRT מספק ממשק מאוחד לשימוש ביחידות עיבוד עצביות (NPU) בלי שתצטרכו לנווט בין קומפיילרים, סביבות זמן ריצה או תלות בספריות ספציפיות לספקים. השימוש ב-LiteRT להאצת NPU משפר את הביצועים של הסקה בזמן אמת ובמודלים גדולים, ומצמצם את העתקות הזיכרון באמצעות שימוש במאגר חומרה ללא העתקה.
שנתחיל?
מודלים קלאסיים של למידת מכונה
לדוגמאות של מודלים קלאסיים של ML, אפשר לעיין באפליקציות ההדגמה הבאות.
- אפליקציית Kotlin לחלוקת תמונות למקטעים: הידור AOT ו-במכשיר(JIT).
- Image segmentation C++ App: הידור AOT ובמכשיר (JIT) באותה אפליקציה.
מודלים של AI גנרטיבי
לגבי מודלים של AI גנרטיבי, אפשר לעיין בהדגמות ובמדריך הבאים:
- EmbeddingGemma semantic similarity C++ App: הסקת מסקנות ב-CPU/GPU/NPU.
- מדריך בנושא הפעלת מודלים גדולים של שפה (LLM) באמצעות LiteRT-LM.
ספקי NPU
LiteRT תומך בהאצת NPU עם הספקים הבאים:
Google Tensor
- תמיכה בהרצת AOT דרך
CompiledModelAPI. - פרטים על ההגדרה מופיעים במאמר Google Tensor.
Qualcomm AI Engine Direct
- תמיכה ב-AOT ובהידור והרצה במכשיר באמצעות
CompiledModelAPI. - פרטים על ההגדרה זמינים במאמר בנושא Qualcomm AI Engine Direct.
- כדי לראות את העדכונים האחרונים, אפשר לעיין במאמר Unlocking Peak Performance on Qualcomm NPU with LiteRT (השגת ביצועים אופטימליים ב-NPU של Qualcomm באמצעות LiteRT).
MediaTek NeuroPilot
- תמיכה ב-AOT ובהידור והרצה במכשיר באמצעות
CompiledModelAPI. - פרטים על ההגדרה זמינים במאמר בנושא MediaTek NeuroPilot.
- אפשר לקרוא את המאמר MediaTek NPU and LiteRT: Powering the next generation of AI במכשיר כדי לקבל את העדכונים האחרונים.
Intel OpenVino
- תמיכה ב-AOT ובהידור והרצה במכשיר באמצעות
CompiledModelAPI. - פרטים על ההגדרה זמינים במאמר בנושא Intel OpenVino.
הידור AOT והידור במכשיר
ה-NPU של LiteRT תומך בהידור AOT ובהידור במכשיר כדי לעמוד בדרישות הפריסה הספציפיות שלכם:
- קומפילציה אופליין (AOT): מתאימה במיוחד למודלים גדולים ומורכבים שבהם ידוע ה-SoC של היעד. קומפילציה מראש מפחיתה באופן משמעותי את עלויות האתחול ואת השימוש בזיכרון כשהמשתמש מפעיל את האפליקציה.
- הידור אונליין (במכשיר): נקרא גם הידור JIT. האפשרות הזו מתאימה במיוחד להפצת מודלים קטנים בלי קשר לפלטפורמה. המודל עובר קומפילציה במכשיר של המשתמש במהלך האתחול, כך שלא נדרש שלב הכנה נוסף, אבל העלות של ההרצה הראשונה גבוהה יותר.
כך אפשר לפרוס את המודל באמצעות אפשרויות של קומפילציה מראש (AOT) או קומפילציה במכשיר:
שלב 1: קומפילציה של AOT עבור מערכות על שבב (SoC) של יחידות NPU
אתם יכולים להשתמש בקומפיילר LiteRT AOT (ahead of time) כדי לקמפל את מודל .tflite אל מערכות SoC נתמכות. אפשר גם לטרגט כמה ספקים וגרסאות של SoC בו-זמנית בתהליך קומפילציה אחד. פרטים נוספים זמינים במחברת ה-LiteRT AOT Compilation. קימפול AOT הוא אופציונלי, אבל מומלץ מאוד למודלים גדולים יותר כדי לקצר את זמן האתחול במכשיר. השלב הזה לא נדרש לקומפילציה במכשיר.
שלב 2: אם משתמשים ב-Android, פורסים את האפליקציה באמצעות Google Play
ב-Android, אפשר להשתמש ב-Google Play for On-device AI (PODAI) כדי לפרוס את המודל ואת ספריות זמן הריצה של NPU עם האפליקציה.
- למודלים של קומפילציה במכשיר: מוסיפים את קובץ המודל המקורי .tflite ישירות לתיקייה assets/ של האפליקציה.
- דוגמה להטמעה מופיעה באפליקציית Kotlin להדרכה של פילוח במכשיר.
- למודלים של קומפילציה של AOT: משתמשים ב-LiteRT כדי לייצא את המודלים שעברו קומפילציה לחבילת AI אחת של Google Play.
לאחר מכן מעלים את חבילת ה-AI ל-Google Play כדי לספק באופן אוטומטי את המודלים המהודרים הנכונים למכשירי המשתמשים.
- הוראות לייצוא מודלים שעברו קומפילציה לחבילת AI של Play זמינות במאמר בנושא מחברת LiteRT AOT Compilation.
- אפשר לראות דוגמה להטמעה באפליקציית Kotlin של קומפילציה מראש (AOT) של פילוח.
- בספריות זמן ריצה של NPU, משתמשים בהפצת פיצ'רים ב-Play כדי להפיץ את ספריות זמן הריצה הנכונות למכשירים של המשתמשים.
בקטעים הבאים מוסבר איך לבצע פריסה באמצעות Play AI Pack ו-הפצת פיצ'רים ב-Play.
פריסת מודלים של AOT באמצעות חבילת ה-AI של Play
השלבים הבאים מתארים איך פורסים מודלים שעברו קומפילציה של AOT באמצעות חבילות AI של Play.
הוספת חבילת AI לפרויקט
כדי לייבא חבילות AI לפרויקט Gradle, מעתיקים את חבילות ה-AI לספריית הבסיס של פרויקט Gradle. לדוגמה:
my_app/
...
ai_packs/
my_model/...
my_model_mtk/...
מוסיפים כל חבילת AI להגדרת ה-build של Gradle:
// my_app/ai_packs/my_model/build.gradle.kts
plugins { id("com.android.ai-pack") }
aiPack {
packName = "my_model" // ai pack dir name
dynamicDelivery { deliveryType = "on-demand" }
}
// Add another build.gradle.kts for my_model_mtk/ as well
הוספת חבילות AI להגדרת Gradle
מעתיקים את device_targeting_configuration.xml מחבילות ה-AI שנוצרו לספרייה של מודול האפליקציה הראשי. ואז מעדכנים את settings.gradle.kts:
// my_app/setting.gradle.kts
...
// AI Packs
include(":ai_packs:my_model")
include(":ai_packs:my_model_mtk")
עדכון build.gradle.kts:
// my_app/build.gradle.kts
android {
...
defaultConfig {
...
// API level 31+ is required for NPU support.
minSdk = 31
}
// AI Packs
assetPacks.add(":ai_packs:my_model")
assetPacks.add(":ai_packs:my_model_mtk")
}
הגדרת חבילת AI למשלוח לפי דרישה
מסירה לפי דרישה מאפשרת לבקש את המודל בזמן הריצה, וזה שימושי אם המודל נדרש רק עבור תהליכי משתמש מסוימים. המודל יורד אל מרחב האחסון הפנימי של האפליקציה. אחרי שמגדירים את התכונה Android AI Pack בקובץ build.gradle.kts, בודקים את היכולות של המכשיר.
אפשר לעיין גם בהוראות בנושא הפצה בזמן ההתקנה וFast-Follow מ-PODAI.
val env = Environment.create(BuiltinNpuAcceleratorProvider(context))
val cpuGpuModelProvider =
ModelProvider.staticModel(
ModelProvider.Type.ASSET,
"model/my_model_cpu_gpu.tflite",
if (accelerator != Accelerator.NPU) accelerator else Accelerator.CPU,
)
val qualcommNpuModelProvider =
AiPackModelProvider(context, "my_model", "model/my_model.tflite")
{
buildSet {
if (
accelerator == Accelerator.NPU && NpuCompatibilityChecker.Qualcomm.isDeviceSupported()
)
add(Accelerator.NPU)
}
}
val mtkNpuModelProvider =
AiPackModelProvider(context, "my_model_mtk", "model/my_model.tflite")
{
buildSet {
if (
accelerator == Accelerator.NPU && NpuCompatibilityChecker.Mediatek.isDeviceSupported()
)
add(Accelerator.NPU)
}
}
val googleTensorTpuModelProvider =
AiPackModelProvider(context, "my_model", "model/my_model.tflite")
{
buildSet {
if (accelerator == Accelerator.NPU &&
NpuCompatibilityChecker.GoogleTensor.isDeviceSupported()
)
add(Accelerator.NPU)
}
}
val aiPackModelProvider =
ModelSelector(cpuGpuModelProvider, mtkNpuModelProvider, qualcommNpuModelProvider, googleTensorTpuModelProvider)
.selectModel(env)
val compiledModel = CompiledModel.create(
model.getPath(),
CompiledModel.Options(model.getCompatibleAccelerators()),
env,
)
פריסת ספריות זמן ריצה של NPU באמצעות הפצת פיצ'רים ב-Play
הפצת פיצ'רים ב-Play תומך בכמה אפשרויות מסירה כדי לבצע אופטימיזציה של גודל ההורדה הראשוני, כולל מסירה בזמן ההתקנה, מסירה על פי דרישה, מסירה מותנית ומסירה מיידית. כאן מוצג מדריך בסיסי לאספקה בזמן ההתקנה.
הוספת ספריות זמן ריצה של NPU לפרויקט
מורידים את litert_npu_runtime_libraries.zip מהגרסה האחרונה לקומפילציה של AOT או את litert_npu_runtime_libraries_jit.zip מהגרסה האחרונה לקומפילציה במכשיר, ומחלצים אותו בספריית הבסיס של הפרויקט:
my_app/
...
litert_npu_runtime_libraries/
google_tensor_runtime/...
mediatek_runtime/...
qualcomm_runtime_v69/...
qualcomm_runtime_v73/...
qualcomm_runtime_v75/...
qualcomm_runtime_v79/...
qualcomm_runtime_v81/...
fetch_qualcomm_library.sh
מריצים את הסקריפט כדי להוריד את ספריות התמיכה של ה-NPU. לדוגמה, מריצים את הפקודה הבאה עבור Qualcomm NPUs:
$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh
הוספת ספריות זמן ריצה של NPU להגדרת Gradle
מעתיקים את device_targeting_configuration.xml מחבילות ה-AI שנוצרו לספרייה של מודול האפליקציה הראשי. ואז מעדכנים את settings.gradle.kts:
// my_app/setting.gradle.kts
...
// NPU runtime libraries
include(":litert_npu_runtime_libraries:runtime_strings")
include(":litert_npu_runtime_libraries:google_tensor_runtime")
include(":litert_npu_runtime_libraries:mediatek_runtime")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v69")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v73")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v75")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v79")
include(":litert_npu_runtime_libraries:qualcomm_runtime_v81")
עדכון build.gradle.kts:
// my_app/build.gradle.kts
android {
...
defaultConfig {
...
// API level 31+ is required for NPU support.
minSdk = 31
// NPU only supports arm64-v8a
ndk { abiFilters.add("arm64-v8a") }
// Needed for Qualcomm NPU runtime libraries
packaging { jniLibs { useLegacyPackaging = true } }
}
// Device targeting
bundle {
deviceTargetingConfig = file("device_targeting_configuration.xml")
deviceGroup {
enableSplit = true // split bundle by #group
defaultGroup = "other" // group used for standalone APKs
}
}
// NPU runtime libraries
dynamicFeatures.add(":litert_npu_runtime_libraries:google_tensor_runtime")
dynamicFeatures.add(":litert_npu_runtime_libraries:mediatek_runtime")
dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v69")
dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v73")
dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v75")
dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v79")
dynamicFeatures.add(":litert_npu_runtime_libraries:qualcomm_runtime_v81")
}
dependencies {
// Dependencies for strings used in the runtime library modules.
implementation(project(":litert_npu_runtime_libraries:runtime_strings"))
...
}
שלב 3: הסקת מסקנות ב-NPU באמצעות LiteRT Runtime
LiteRT מפשט את הפיתוח בהתאם לגרסאות ספציפיות של SoC, ומאפשר להריץ את המודל ב-NPU באמצעות כמה שורות קוד בלבד. הוא גם מספק מנגנון חזק ומוטמע לחזרה למצב הקודם: אפשר לציין CPU, GPU או את שניהם כאפשרויות, ו-LiteRT ישתמש בהם באופן אוטומטי אם ה-NPU לא זמין. בנוסף, קומפילציית AOT תומכת גם בגיבוי. היא מספקת האצלת סמכויות חלקית ב-NPU, שבה תתי-גרפים לא נתמכים מופעלים בצורה חלקה ב-CPU או ב-GPU, בהתאם להגדרות.
הפעלה ב-Kotlin
דוגמאות להטמעה אפשר לראות באפליקציות ההדגמה הבאות:
הוספת יחסי תלות ב-Android
אפשר להוסיף את חבילת LiteRT Maven העדכנית לתלויות (dependencies) build.gradle:
dependencies {
...
implementation("com.google.ai.edge.litert:litert:+")
}
שילוב בזמן ריצה
// 1. Load model and initialize runtime.
// If NPU is unavailable, inference will fallback to GPU.
val model =
CompiledModel.create(
context.assets,
"model/mymodel.tflite",
CompiledModel.Options(Accelerator.NPU, Accelerator.GPU)
)
// 2. Pre-allocate input/output buffers
val inputBuffers = model.createInputBuffers()
val outputBuffers = model.createOutputBuffers()
// 3. Fill the first input
inputBuffers[0].writeFloat(...)
// 4. Invoke
model.run(inputBuffers, outputBuffers)
// 5. Read the output
val outputFloatArray = outputBuffers[0].readFloat()
הרצה בפלטפורמות שונות ב-C++
אפשר לראות דוגמה להטמעה באפליקציית C++ לפילוח אסינכרוני.
יחסי תלות ב-Bazel Build
משתמשי C++ צריכים ליצור את יחסי התלות של האפליקציה באמצעות האצת LiteRT NPU. כלל cc_binary שמכיל את הלוגיקה הבסיסית של האפליקציה (לדוגמה, main.cc) דורש את רכיבי זמן הריצה הבאים:
- LiteRT C API shared library: המאפיין
dataצריך לכלול את ספריית ה-LiteRT C API shared library (//litert/c:litert_runtime_c_api_shared_lib) ואת האובייקט המשותף של השליחה הספציפי לספק עבור ה-NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so). - ספריות backend ספציפיות ל-NPU: לדוגמה, ספריות Qualcomm AI RT (QAIRT) למארח Android (כמו
libQnnHtp.so, libQnnHtpPrepare.so) והספרייה התואמת Hexagon DSP (libQnnHtpV79Skel.so). כך מובטח שזמן הריצה של LiteRT יוכל להעביר חישובים ל-NPU. - יחסי תלות של מאפיינים: המאפיין
depsמקשר ליחסי תלות חיוניים בזמן ההידור, כמו מאגר טנסורים של LiteRT (//litert/cc:litert_tensor_buffer) וה-API של שכבת השליחה של ה-NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). כך קוד האפליקציה יכול ליצור אינטראקציה עם ה-NPU דרך LiteRT. - קבצים של מודלים ונכסים אחרים: נכללים באמצעות המאפיין
data.
ההגדרה הזו מאפשרת לבינארי המהודר לטעון באופן דינמי את ה-NPU ולהשתמש בו כדי להסיק מסקנות בלמידת מכונה מואצת.
הגדרת סביבת NPU
חלק מהעורפים של NPU דורשים ספריות או תלות בזמן ריצה. כשמשתמשים ב-API של מודל שעבר קומפילציה, LiteRT מארגן את הדרישות האלה באמצעות אובייקט Environment.
אפשר להשתמש בקוד הבא כדי למצוא את ספריות ה-NPU או מנהלי ההתקנים המתאימים:
// Provide a dispatch library directory (following is a hypothetical path) for the NPU
std::vector<Environment::Option> environment_options = {
{
Environment::OptionTag::DispatchLibraryDir,
"/usr/lib64/npu_dispatch/"
}
};
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create(absl::MakeConstSpan(environment_options)));
שילוב בזמן ריצה
בקטע הקוד הבא מוצגת הטמעה בסיסית של התהליך כולו ב-C++:
// 1. Load the model that has NPU-compatible ops
LITERT_ASSIGN_OR_RETURN(auto model, Model::Load("mymodel_npu.tflite"));
// 2. Create a compiled model with NPU acceleration
// See following section on how to set up NPU environment
LITERT_ASSIGN_OR_RETURN(auto compiled_model,
CompiledModel::Create(env, model, kLiteRtHwAcceleratorNpu));
// 3. Allocate I/O buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());
// 4. Fill model inputs (CPU array -> NPU buffers)
float input_data[] = { /* your input data */ };
input_buffers[0].Write<float>(absl::MakeConstSpan(input_data, /*size*/));
// 5. Run inference
compiled_model.Run(input_buffers, output_buffers);
// 6. Access model output
std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));
העתקה ללא העברה עם האצה של NPU
שימוש בשיטת העתקה אפסית מאפשר ליחידת NPU לגשת לנתונים ישירות בזיכרון שלה בלי שה-CPU יצטרך להעתיק את הנתונים באופן מפורש. היעדר העתקה של נתונים לזיכרון המעבד וממנו מאפשר לצמצם משמעותית את זמן האחזור מקצה לקצה.
הקוד הבא הוא דוגמה להטמעה של NPU עם העתקה אפסית באמצעות AHardwareBuffer, שמעביר נתונים ישירות ל-NPU. ההטמעה הזו מונעת נסיעות הלוך ושוב יקרות לזיכרון המעבד, ומפחיתה באופן משמעותי את תקורה ההסקה.
// Suppose you have AHardwareBuffer* ahw_buffer
LITERT_ASSIGN_OR_RETURN(auto tensor_type, model.GetInputTensorType("input_tensor"));
LITERT_ASSIGN_OR_RETURN(auto npu_input_buffer, TensorBuffer::CreateFromAhwb(
env,
tensor_type,
ahw_buffer,
/* offset = */ 0
));
std::vector<TensorBuffer> input_buffers{npu_input_buffer};
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());
// Execute the model
compiled_model.Run(input_buffers, output_buffers);
// Retrieve the output (possibly also an AHWB or other specialized buffer)
auto ahwb_output = output_buffers[0].GetAhwb();
שרשור של כמה מסקנות של NPU
בצינורות מורכבים, אפשר לשרשר כמה מסקנות של NPU. מכיוון שבכל שלב נעשה שימוש במאגר שמתאים להאצה, הצינור נשאר ברובו בזיכרון שמנוהל על ידי NPU:
// compiled_model1 outputs into an AHWB
compiled_model1.Run(input_buffers, intermediate_buffers);
// compiled_model2 consumes that same AHWB
compiled_model2.Run(intermediate_buffers, final_outputs);
שמירת מטמון של הידור במכשיר באמצעות NPU
LiteRT תומך בהידור (קומפילציה) של מודלים של .tflite במכשיר (שנקרא JIT) ב-NPU.
הידור JIT יכול להיות שימושי במיוחד במצבים שבהם אי אפשר להדר את המודל מראש.
עם זאת, הידור JIT עלול לגרום לזמן טעינה ולתקורה בזיכרון, כי הוא מתרגם את המודל שסופק על ידי המשתמש להוראות בייטקוד של NPU על פי דרישה. כדי למזער את ההשפעה על הביצועים, אפשר לשמור במטמון את תוצרי הקומפילציה של ה-NPU.
כשהשמירה במטמון מופעלת, LiteRT יפעיל את ההידור מחדש של המודל רק כשצריך, למשל:
- הגרסה של תוסף מהדר ה-NPU של הספק השתנתה.
- המאפיינים הייחודיים של גרסת ה-build של Android השתנו.
- המודל שסופק על ידי המשתמש השתנה;
- אפשרויות ההידור השתנו.
כדי להפעיל שמירת מטמון של קומפילציה של NPU, מציינים את תג הסביבה CompilerCacheDir באפשרויות הסביבה. הערך צריך להיות נתיב קיים שאפשר לכתוב בו באפליקציה.
const std::array environment_options = {
litert::Environment::Option{
/*.tag=*/litert::Environment::OptionTag::CompilerPluginLibraryDir,
/*.value=*/kCompilerPluginLibSearchPath,
},
litert::Environment::Option{
litert::Environment::OptionTag::DispatchLibraryDir,
kDispatchLibraryDir,
},
// 'kCompilerCacheDir' will be used to store NPU-compiled model
// artifacts.
litert::Environment::Option{
litert::Environment::OptionTag::CompilerCacheDir,
kCompilerCacheDir,
},
};
// Create an environment.
LITERT_ASSERT_OK_AND_ASSIGN(
auto environment, litert::Environment::Create(environment_options));
// Load a model.
auto model_path = litert::testing::GetTestFilePath(kModelFileName);
LITERT_ASSERT_OK_AND_ASSIGN(auto model,
litert::Model::CreateFromFile(model_path));
// Create a compiled model, which only triggers NPU compilation if
// required.
LITERT_ASSERT_OK_AND_ASSIGN(
auto compiled_model, litert::CompiledModel::Create(
environment, model, kLiteRtHwAcceleratorNpu));
דוגמה לחיסכון בזיכרון ולזמן האחזור:
הזמן והזיכרון שנדרשים לקומפילציה של NPU יכולים להשתנות בהתאם לכמה גורמים, כמו שבב ה-NPU הבסיסי, המורכבות של מודל הקלט וכו'.
בטבלה הבאה מוצגת השוואה בין זמן האתחול של זמן הריצה וצריכת הזיכרון כשנדרשת קומפילציה של NPU לבין המצב שבו אפשר לדלג על הקומפילציה בגלל שמירה במטמון. במכשיר לדוגמה אחד קיבלנו את הנתונים הבאים:
| מודל TFLite | מודל init עם קומפילציה של NPU | model init with cached compilation | הזיכרון שבשימוש בתהליך האתחול עם קומפילציה של NPU | init memory with cached compilation |
|---|---|---|---|---|
| torchvision_resnet152.tflite | 7465.22 אלפיות השנייה | 198.34 אלפיות השנייה | 1525.24MB | 355.07MB |
| torchvision_lraspp_mobilenet_v3_large.tflite | 1,592.54 אלפיות השנייה | 166.47 אלפיות השנייה | 254.90MB | 33.78MB |
במכשיר אחר אנחנו מקבלים את הפרטים הבאים:
| מודל TFLite | מודל init עם קומפילציה של NPU | model init with cached compilation | הזיכרון שבשימוש בתהליך האתחול עם קומפילציה של NPU | init memory with cached compilation |
|---|---|---|---|---|
| torchvision_resnet152.tflite | 2766.44 אלפיות השנייה | 379.86 אלפיות השנייה | 653.54MB | 501.21 MB |
| torchvision_lraspp_mobilenet_v3_large.tflite | 784.14 אלפיות השנייה | 231.76 אלפיות השנייה | 113.14MB | 67.49MB |