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

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

שנתחיל?

כדי להתחיל, אפשר לעיין במדריך הבא שמספק סקירה כללית על NPU:

לדוגמאות להטמעה של LiteRT עם תמיכה ב-NPU, אפשר לעיין באפליקציות ההדגמה הבאות:

ספקי NPU

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

Qualcomm AI Engine Direct

  • נתיבי ביצוע של קימפול AOT וקימפול במכשיר נתמכים באמצעות Compiled Model API.
  • פרטים על ההגדרה מופיעים במאמר בנושא Qualcomm AI Engine Direct.

MediaTek NeuroPilot

  • נתיבי ביצוע AOT ו-JIT נתמכים דרך Compiled Model API.
  • פרטים על ההגדרה זמינים במאמר בנושא MediaTek NeuroPilot.

המרת מודלים והידור שלהם ל-NPU

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

אחרי שממירים ומקמפלים את המודלים, אפשר להשתמש ב-Play for On-device AI (PODAI) כדי להעלות מודלים ל-Google Play ולספק מודלים למכשירים באמצעות המסגרת On-Demand AI.

מחברת LiteRT AOT compilation כוללת מדריך מקיף להמרת מודלים ולהידור שלהם ל-NPU.

‫[AOT בלבד] פריסה באמצעות חבילת AI ל-Play

אחרי שממירים את המודל ומקמפלים חבילת AI, אפשר לפעול לפי השלבים הבאים כדי לפרוס את חבילת ה-AI ב-Google Play.

ייבוא חבילות 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

הוספת ספריות זמן ריצה של NPU לפרויקט

מורידים את litert_npu_runtime_libraries.zip ל-AOT או את litert_npu_runtime_libraries_jit.zip ל-JIT, ומחלצים אותו בספריית השורש של הפרויקט:

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

הוספת חבילות AI וספריות זמן ריצה של NPU להגדרת Gradle

מעתיקים את device_targeting_configuration.xml מחבילות ה-AI שנוצרו לספרייה של מודול האפליקציה הראשי. ואז מעדכנים את settings.gradle.kts:

// my_app/setting.gradle.kts

...

// [AOT only]
// AI Packs
include(":ai_packs:my_model")
include(":ai_packs:my_model_mtk")

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

  // [AOT Only]
  // AI Packs
  assetPacks.add(":ai_packs:my_model")
  assetPacks.add(":ai_packs:my_model_mtk")

  // 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"))
  ...
}

[AOT בלבד] שימוש בפריסה על פי דרישה

אם הגדרתם את התכונה Android AI Pack בקובץ build.gradle.kts, תוכלו לבדוק את היכולות של המכשיר ולהשתמש ב-NPU במכשירים מתאימים, תוך שימוש ב-GPU וב-CPU כגיבוי:

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,
)

יצירת CompiledModel למצב JIT

val env = Environment.create(BuiltinNpuAcceleratorProvider(context))

val compiledModel = CompiledModel.create(
    "model/my_model.tflite",
    CompiledModel.Options(Accelerator.NPU),
    env,
)

הסקת מסקנות ב-NPU באמצעות LiteRT ב-Kotlin

כדי להתחיל להשתמש בהאצת NPU, צריך להעביר את הפרמטר NPU כשיוצרים את המודל המהודר (CompiledModel).

בקטע הקוד הבא מוצגת הטמעה בסיסית של התהליך כולו ב-Kotlin:

val inputBuffers = model.createInputBuffers()
val outputBuffers = model.createOutputBuffers()

inputBuffers[0].writeFloat(FloatArray(data_size) { data_value })
model.run(inputBuffers, outputBuffers)
val outputFloatArray = outputBuffers[0].readFloat()

inputBuffers.forEach { it.close() }
outputBuffers.forEach { it.close() }
model.close()

הסקת מסקנות ב-NPU באמצעות LiteRT ב-C++‎

יחסי תלות של 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).
  • ספריות קצה עורפיות ספציפיות ל-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 just-in-time‏ (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