האצת NPU באמצעות LiteRT

‫LiteRT מספק ממשק מאוחד לשימוש ביחידות עיבוד עצביות (NPU) בלי שתצטרכו לנווט בקומפיילרים, בסביבות זמן ריצה או בתלות בספריות ספציפיות לספקים. השימוש ב-LiteRT להאצת NPU משפר את הביצועים של הסקה בזמן אמת ובמודלים גדולים, ומצמצם את העתקות הזיכרון באמצעות שימוש במאגר חומרה ללא העתקה.

שנתחיל?

ספקי NPU

‫LiteRT תומך בהאצת NPU עם הספקים הבאים:

Qualcomm AI Engine Direct

MediaTek NeuroPilot

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/ של האפליקציה.
  • למודלים של קומפילציה מסוג AOT: משתמשים ב-LiteRT כדי לייצא את המודלים שעברו קומפילציה לחבילת AI אחת של Google Play. לאחר מכן מעלים את חבילת ה-AI ל-Google Play כדי לספק באופן אוטומטי את המודלים המהודרים הנכונים למכשירים של המשתמשים.
  • בספריות זמן ריצה של 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