LiteRT מספק ממשק מאוחד לשימוש ביחידות עיבוד עצביות (NPU) בלי שתצטרכו לנווט בקומפיילרים, בסביבות זמן ריצה או בתלות בספריות ספציפיות לספקים. השימוש ב-LiteRT להאצת NPU משפר את הביצועים של הסקה בזמן אמת ובמודלים גדולים, ומצמצם את העתקות הזיכרון באמצעות שימוש במאגר חומרה ללא העתקה.
שנתחיל?
- למודלים קלאסיים של ML, אפשר לעיין באפליקציות ההדגמה הבאות.
- אפליקציית Kotlin לפילוח תמונות: מספקת דוגמאות להידור AOT ולהידור במכשיר (JIT), בהתאמה.
- אפליקציית C++ לפילוח אסינכרוני: מדגימה הידור AOT והידור במכשיר (JIT) באותה אפליקציה.
- למודלים גדולים של שפה (LLM), אפשר לעיין במדריך להפעלת LLM ב-NPU באמצעות LiteRT-LM.
ספקי NPU
LiteRT תומך בהאצת NPU עם הספקים הבאים:
Qualcomm AI Engine Direct
- תמיכה ב-AOT ובהפעלת קומפילציה במכשיר באמצעות
CompiledModelAPI. - פרטים על ההגדרה זמינים במאמר בנושא Qualcomm AI Engine Direct.
- כדי לראות את העדכונים האחרונים, אפשר לעיין במאמר Unlocking Peak Performance on Qualcomm NPU with LiteRT.
MediaTek NeuroPilot
- תמיכה ב-AOT ובהפעלת קומפילציה במכשיר באמצעות
CompiledModelAPI. - פרטים על ההגדרה זמינים במאמר בנושא MediaTek NeuroPilot.
- אפשר לקרוא את המאמר MediaTek NPU and LiteRT: Powering the next generation of on-device AI כדי לקבל את העדכונים האחרונים.
Google Tensor
Google Tensor SDK הוא בגישה ניסיונית. תוכלו להירשם כאן.
קומפילציה מראש (AOT) וקומפילציה במכשיר
ה-NPU של LiteRT תומך בהידור AOT ובהידור במכשיר כדי לעמוד בדרישות הפריסה הספציפיות שלכם:
- קומפילציה אופליין (AOT): מתאים במיוחד למודלים גדולים ומורכבים שבהם ידוע ה-SoC של היעד. הקומפילציה מראש מפחיתה באופן משמעותי את עלויות האתחול ואת השימוש בזיכרון כשהמשתמש מפעיל את האפליקציה.
- הידור אונליין (במכשיר): נקרא גם הידור JIT. האפשרות הזו מתאימה במיוחד להפצת מודלים קטנים שלא תלויים בפלטפורמה. המודל עובר קומפילציה במכשיר של המשתמש במהלך האתחול, כך שלא נדרש שלב הכנה נוסף, אבל העלות של ההרצה הראשונה גבוהה יותר.
במדריך הבא מוסבר איך לבצע פריסה עבור קומפילציה מראש (AOT) וקומפילציה במכשיר בשלושה שלבים.
שלב 1: קומפילציה של AOT עבור מערכות על שבב (SoC) של יחידות NPU
אתם יכולים להשתמש בקומפיילר LiteRT AOT (קומפיילר מראש) כדי לקמפל את מודל .tflite אל מערכות SoC נתמכות. אפשר גם לטרגט כמה ספקים וגרסאות של SoC בו-זמנית בתהליך קומפילציה אחד. פרטים נוספים זמינים במחברת ה-LiteRT AOT Compilation. קימפול AOT הוא אופציונלי, אבל מומלץ מאוד למודלים גדולים יותר כדי לקצר את זמן האתחול במכשיר. השלב הזה לא נדרש עבור קומפילציה במכשיר.
שלב 2: אם משתמשים ב-Android, פורסים את האפליקציה באמצעות Google Play
ב-Android, אפשר להשתמש ב-Google Play for On-device AI (PODAI) כדי לפרוס את המודל ואת ספריות זמן הריצה של NPU עם האפליקציה.
- למודלים של קומפילציה במכשיר: מוסיפים את קובץ המודל המקורי .tflite ישירות לתיקייה assets/ של האפליקציה.
- אפשר לראות דוגמה להטמעה ב-Segmentation on-device compilation Kotlin App.
- למודלים של קומפילציה מסוג AOT: משתמשים ב-LiteRT כדי לייצא את המודלים שעברו קומפילציה לחבילת AI אחת של Google Play.
לאחר מכן מעלים את חבילת ה-AI ל-Google Play כדי לספק באופן אוטומטי את המודלים המהודרים הנכונים למכשירים של המשתמשים.
- הוראות לייצוא מודלים שעברו קומפילציה אל חבילת AI של Play זמינות במאמר בנושא מחברת LiteRT AOT Compilation.
- אפשר לראות דוגמה להטמעה באפליקציית Kotlin של פילוח קומפילציה AOT.
- בספריות זמן ריצה של NPU, משתמשים בPlay Feature Delivery כדי להפיץ את ספריות זמן הריצה הנכונות למכשירים של המשתמשים.
בקטעים הבאים מוסבר איך להטמיע את התכונות האלה באמצעות Play AI Pack ו-Play Feature Delivery.
פריסת מודלים של 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, בודקים את היכולות של המכשיר.
אפשר לעיין גם בהוראות בנושא הפצה בזמן ההתקנה והפצה מהירה מ-PODAI.
val env = Environment.create(BuiltinNpuAcceleratorProvider(context))
val modelProvider = AiPackModelProvider(
context, "my_model", "model/my_model.tflite") {
if (NpuCompatibilityChecker.Qualcomm.isDeviceSupported())
setOf(Accelerator.NPU) else setOf(Accelerator.CPU, Accelerator.GPU)
}
val mtkModelProvider = AiPackModelProvider(
context, "my_model_mtk", "model/my_model_mtk.tflite") {
if (NpuCompatibilityChecker.Mediatek.isDeviceSupported())
setOf(Accelerator.NPU) else setOf()
}
val modelSelector = ModelSelector(modelProvider, mtkModelProvider)
val model = modelSelector.selectModel(env)
val compiledModel = CompiledModel.create(
model.getPath(),
CompiledModel.Options(model.getCompatibleAccelerators()),
env,
)
פריסת ספריות זמן ריצה של NPU באמצעות Play Feature Delivery
Play Feature Delivery תומך בכמה אפשרויות מסירה כדי לבצע אופטימיזציה של גודל ההורדה הראשוני, כולל מסירה בזמן ההתקנה, מסירה על פי דרישה, מסירה מותנית ומסירה מיידית. כאן מוצג מדריך בסיסי לאספקה בזמן ההתקנה.
הוספת ספריות זמן ריצה של NPU לפרויקט
מורידים את הקובץ litert_npu_runtime_libraries.zip למהדר AOT או את הקובץ litert_npu_runtime_libraries_jit.zip למהדר במכשיר, ומחלצים אותו לתיקיית השורש של הפרויקט:
my_app/
...
litert_npu_runtime_libraries/
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: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: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 העדכנית ליחסי התלות של 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++
אפשר לראות דוגמה להטמעה ב-Asynchronous segmentation C++ App.
יחסי תלות ב-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 תומך בהידור NPU במכשיר (שנקרא JIT) של מודלים של .tflite. הידור 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 |