Accelerazione della NPU con LiteRT

LiteRT fornisce un'interfaccia unificata per utilizzare le unità di elaborazione neurali (NPU) senza costringerti a navigare tra compilatori, runtime o dipendenze di librerie specifici del fornitore. L'utilizzo di LiteRT per l'accelerazione della NPU migliora le prestazioni per l'inferenza in tempo reale e di modelli di grandi dimensioni e riduce al minimo le copie della memoria tramite l'utilizzo di buffer hardware senza copia.

Inizia

Per iniziare, consulta la guida generale alla NPU:

  • Per i modelli di machine learning classici, consulta le sezioni seguenti per i passaggi di conversione, compilazione e deployment.
  • Per i modelli linguistici di grandi dimensioni (LLM), utilizza il nostro framework LiteRT-LM:

Per esempi di implementazioni di LiteRT con supporto NPU, consulta le seguenti applicazioni demo:

Fornitori di NPU

LiteRT supporta l'accelerazione NPU con i seguenti fornitori:

Qualcomm AI Engine Direct

  • I percorsi di esecuzione della compilazione AOT e On-Device sono supportati tramite l'API Compiled Model.
  • Per i dettagli della configurazione, consulta Qualcomm AI Engine Direct.

MediaTek NeuroPilot

  • I percorsi di esecuzione AOT e JIT sono supportati tramite l'API Compiled Model.
  • Per i dettagli della configurazione, vedi MediaTek NeuroPilot.

Convertire e compilare modelli per la NPU

Per utilizzare l'accelerazione NPU con LiteRT, i modelli devono essere convertiti nel formato file LiteRT e compilati per l'utilizzo della NPU sul dispositivo. Puoi utilizzare il compilatore LiteRT AOT (ahead of time) per compilare i modelli in un pacchetto AI, che raggruppa i modelli compilati con le configurazioni di targeting per dispositivo. In questo modo si verifica che i modelli vengano forniti correttamente ai dispositivi a seconda che siano dotati o ottimizzati per SoC particolari.

Dopo aver convertito e compilato i modelli, puoi utilizzare Play for On-device AI (PODAI) per caricare i modelli su Google Play e distribuirli ai dispositivi tramite il framework On-Demand AI.

Utilizza il notebook di compilazione LiteRT AOT per una guida end-to-end alla conversione e alla compilazione di modelli per la NPU.

[Solo AOT] Esegui il deployment con Play AI Pack

Dopo aver convertito il modello e compilato un pacchetto AI, segui questi passaggi per implementare il pacchetto AI con Google Play.

Importa i pacchetti AI nel progetto Gradle

Copia i pacchetti AI nella directory principale del progetto Gradle. Ad esempio:

my_app/
    ...
    ai_packs/
        my_model/...
        my_model_mtk/...

Aggiungi ogni pacchetto AI alla configurazione di build di 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

Aggiungi le librerie di runtime della NPU al progetto

Scarica litert_npu_runtime_libraries.zip per AOT o litert_npu_runtime_libraries_jit.zip per JIT e decomprimilo nella directory principale del progetto:

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

Esegui lo script per scaricare le librerie di supporto della NPU. Ad esempio, esegui il seguente comando per le NPU Qualcomm:

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Aggiungi pacchetti AI e librerie di runtime NPU alla configurazione Gradle

Copia device_targeting_configuration.xml dai pacchetti AI generati nella directory del modulo dell'app principale. Poi aggiorna 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")

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

[Solo AOT] Utilizza il deployment on demand

Con la funzionalità Android AI Pack configurata nel file build.gradle.kts, controlla le funzionalità del dispositivo e utilizza la NPU sui dispositivi compatibili, utilizzando GPU e CPU come fallback:

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

Crea CompiledModel per la modalità JIT

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

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

Inferenza sulla NPU utilizzando LiteRT in Kotlin

Per iniziare a utilizzare l'acceleratore NPU, passa il parametro NPU durante la creazione del modello compilato (CompiledModel).

Il seguente snippet di codice mostra un'implementazione di base dell'intero processo in 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()

Inferenza sulla NPU utilizzando LiteRT in C++

Dipendenze build

Gli utenti C++ devono creare le dipendenze dell'applicazione con l'accelerazione LiteRT NPU. La regola cc_binary che raggruppa la logica dell'applicazione principale (ad es. main.cc) richiede i seguenti componenti di runtime:

  • Libreria condivisa dell'API LiteRT C: l'attributo data deve includere la libreria condivisa dell'API LiteRT C (//litert/c:litert_runtime_c_api_shared_lib) e l'oggetto condiviso di distribuzione specifico del fornitore per la NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so).
  • Librerie di backend specifiche per la NPU: ad esempio, le librerie Qualcomm AI RT (QAIRT) per l'host Android (come libQnnHtp.so, libQnnHtpPrepare.so) e la libreria DSP Hexagon corrispondente (libQnnHtpV79Skel.so). In questo modo, il runtime LiteRT può scaricare i calcoli sulla NPU.
  • Dipendenze degli attributi: l'attributo deps si collega a dipendenze essenziali in fase di compilazione, come il buffer tensore di LiteRT (//litert/cc:litert_tensor_buffer) e l'API per il livello di distribuzione della NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). In questo modo, il codice dell'applicazione può interagire con la NPU tramite LiteRT.
  • File del modello e altri asset: inclusi tramite l'attributo data.

Questa configurazione consente al binario compilato di caricare e utilizzare dinamicamente la NPU per l'inferenza di machine learning accelerata.

Configurare un ambiente NPU

Alcuni backend NPU richiedono librerie o dipendenze di runtime. Quando utilizzi l'API modello compilato, LiteRT organizza questi requisiti tramite un oggetto Environment. Utilizza il seguente codice per trovare le librerie o i driver NPU appropriati:

// 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)));

Integrazione di runtime

Il seguente snippet di codice mostra un'implementazione di base dell'intera procedura in 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));

Zero-copy con accelerazione NPU

L'utilizzo della copia zero consente a un'NPU di accedere direttamente ai dati nella propria memoria senza che la CPU debba copiarli esplicitamente. Non copiando i dati nella memoria della CPU e dalla memoria della CPU, la copia zero può ridurre significativamente la latenza end-to-end.

Il seguente codice è un'implementazione di esempio di Zero-Copy NPU con AHardwareBuffer, che passa i dati direttamente alla NPU. Questa implementazione evita costosi round trip alla memoria della CPU, riducendo significativamente l'overhead di inferenza.

// 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();

Concatenare più inferenze della NPU

Per pipeline complesse, puoi concatenare più inferenze della NPU. Poiché ogni passaggio utilizza un buffer compatibile con l'acceleratore, la pipeline rimane principalmente nella memoria gestita dalla 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);

Memorizzazione nella cache della compilazione just-in-time della NPU

LiteRT supporta la compilazione just-in-time (JIT) della NPU dei modelli .tflite. La compilazione JIT può essere particolarmente utile nelle situazioni in cui la compilazione del modello in anticipo non è fattibile.

La compilazione JIT, tuttavia, può comportare una certa latenza e un sovraccarico di memoria per tradurre il modello fornito dall'utente in istruzioni bytecode NPU su richiesta. Per ridurre al minimo l'impatto sulle prestazioni, gli artefatti di compilazione della NPU possono essere memorizzati nella cache.

Quando la memorizzazione nella cache è abilitata, LiteRT attiverà la ricompilazione del modello solo quando necessario, ad esempio:

  • La versione del plug-in del compilatore NPU del fornitore è cambiata.
  • Il fingerprint build di Android è cambiato.
  • Il modello fornito dall'utente è cambiato.
  • Le opzioni di compilazione sono cambiate.

Per attivare la memorizzazione nella cache della compilazione della NPU, specifica il tag dell'ambiente CompilerCacheDir nelle opzioni dell'ambiente. Il valore deve essere impostato su un percorso scrivibile esistente dell'applicazione.

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

Esempio di risparmio di latenza e memoria:

Il tempo e la memoria necessari per la compilazione della NPU possono variare in base a diversi fattori, come il chip NPU sottostante, la complessità del modello di input e così via.

La seguente tabella confronta il tempo di inizializzazione del runtime e il consumo di memoria quando è richiesta la compilazione della NPU rispetto a quando la compilazione può essere ignorata a causa della memorizzazione nella cache. Su un dispositivo di esempio otteniamo quanto segue:

Modello TFLite model init with NPU compilation model init with cached compilation init memory footprint with NPU compilation init memory with cached compilation
torchvision_resnet152.tflite 7465,22 ms 198,34 ms 1525,24 MB 355,07 MB
torchvision_lraspp_mobilenet_v3_large.tflite 1592,54 ms 166,47 ms 254,90 MB 33,78 MB

Su un altro dispositivo otteniamo quanto segue:

Modello TFLite model init with NPU compilation model init with cached compilation init memory footprint with NPU compilation init memory with cached compilation
torchvision_resnet152.tflite 2766,44 ms 379,86 ms 653,54 MB 501,21 MB
torchvision_lraspp_mobilenet_v3_large.tflite 784,14 ms 231,76 ms 113,14 MB 67,49 MB