Accelerazione della NPU con LiteRT

LiteRT fornisce un'interfaccia unificata per utilizzare le NPU (Neural Processing Unit) senza richiedere di 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 di memoria tramite l'utilizzo di buffer hardware zero-copy.

Inizia

Modelli ML classici

Per i modelli ML classici, consulta le seguenti applicazioni demo.

Modelli di AI generativa

Per i modelli di AI generativa, consulta le seguenti demo e la seguente guida:

Fornitori di NPU

LiteRT supporta l'accelerazione della NPU con i seguenti fornitori:

Google Tensor

  • Supporta l'esecuzione AOT tramite l'API CompiledModel.
  • Per i dettagli sulla configurazione, consulta Google Tensor.

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Intel OpenVino

  • Supporta l'esecuzione della compilazione AOT e on-device tramite l'API CompiledModel.
  • Per i dettagli sulla configurazione, consulta Intel OpenVino.

Compilazione AOT e on-device

La NPU LiteRT supporta sia la compilazione AOT sia quella on-device per soddisfare i requisiti di deployment specifici:

  • Compilazione offline (AOT): è più adatta per modelli di grandi dimensioni e complessi in cui è noto il SoC di destinazione. La compilazione ahead-of-time riduce significativamente i costi di inizializzazione e la memoria utilizzata quando l'utente avvia l'app.
  • Compilazione online (on-device): nota anche come compilazione JIT. È ideale per la distribuzione di modelli di piccole dimensioni indipendenti dalla piattaforma. Il modello viene compilato sul dispositivo dell'utente durante l'inizializzazione, senza richiedere passaggi di preparazione aggiuntivi, ma comportando un costo di prima esecuzione più elevato.

Ecco come puoi eseguire il deployment del modello utilizzando le opzioni di compilazione AOT o on-device:

Passaggio 1: compilazione AOT per i SoC NPU di destinazione

Puoi utilizzare il compilatore AOT (Ahead-Of-Time) LiteRT per compilare il modello .tflite nei SoC supportati. Puoi anche scegliere come target più fornitori e versioni di SoC contemporaneamente in un unico processo di compilazione. Per maggiori dettagli, consulta questo notebook di compilazione AOT LiteRT. Sebbene sia facoltativa, la compilazione AOT è vivamente consigliata per i modelli più grandi per ridurre il tempo di inizializzazione on-device. Questo passaggio non è obbligatorio per la compilazione on-device.

Passaggio 2: esegui il deployment con Google Play su Android

Su Android, utilizza Google Play per l'AI on-device (PODAI) per eseguire il deployment del modello e delle librerie di runtime NPU con la tua app.

  • Per i modelli di compilazione on-device: aggiungi il file del modello .tflite originale direttamente alla directory assets/ dell'app.
  • Per i modelli di compilazione AOT: utilizza LiteRT per esportare i modelli compilati in un unico pacchetto AI di Google Play. Poi carica il pacchetto AI su Google Play per distribuire automaticamente i modelli compilati corretti sui dispositivi degli utenti.
  • Per le librerie di runtime NPU, utilizza Play Feature Delivery per distribuire le librerie di runtime corrette sui dispositivi degli utenti.

Consulta le sezioni seguenti su come eseguire il deployment con Play AI Pack e Play Feature Delivery.

Esegui il deployment dei modelli AOT con Play AI Pack

I passaggi seguenti ti guidano nell'esecuzione del deployment dei modelli compilati AOT utilizzando Play AI Pack.

Aggiungi il pacchetto AI al progetto

Importa i pacchetti AI nel progetto Gradle copiandoli 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 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 i pacchetti AI 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

...
// AI Packs
include(":ai_packs:my_model")
include(":ai_packs:my_model_mtk")

Aggiorna 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")
}

Configura il pacchetto AI per la pubblicazione on demand

La pubblicazione on demand ti consente di richiedere il modello in fase di runtime, il che è utile se il modello è necessario solo per determinati flussi utente. Il modello verrà scaricato nella memoria interna dell'app. Con la funzionalità Pacchetto AI Android configurata nel file build.gradle.kts, controlla le funzionalità del dispositivo. Consulta anche le istruzioni per la pubblicazione al momento dell'installazione e la pubblicazione fast-follow da 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,
)

Esegui il deployment delle librerie di runtime NPU con Play Feature Delivery

Play Feature Delivery supporta più opzioni di pubblicazione per ottimizzare le dimensioni di download iniziali, tra cui la pubblicazione al momento dell'installazione, la pubblicazione on demand, la pubblicazione condizionale e la pubblicazione immediata. Qui mostriamo la guida di base alla pubblicazione al momento dell'installazione.

Aggiungi le librerie di runtime NPU al progetto

Scarica litert_npu_runtime_libraries.zip dalla latest release per la compilazione AOT o litert_npu_runtime_libraries_jit.zip dalla latest release per la compilazione on-device ed estraili nella directory principale del progetto:

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

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

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Aggiungi le 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

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

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

Passaggio 3: inferenza sulla NPU utilizzando il runtime LiteRT

LiteRT astrae la complessità dello sviluppo rispetto a versioni SoC specifiche, consentendoti di eseguire il modello sulla NPU con poche righe di codice. Fornisce anche un meccanismo di fallback integrato e robusto: puoi specificare CPU, GPU o entrambe come opzioni e LiteRT le utilizzerà automaticamente se la NPU non è disponibile. La compilazione AOT supporta anche il fallback. Fornisce una delega parziale sulla NPU in cui i sottografi non supportati vengono eseguiti senza problemi su CPU o GPU come specificato.

Esegui in Kotlin

Consulta l'esempio di implementazione nelle seguenti app demo:

Aggiungi dipendenze Android

Puoi aggiungere l'ultimo pacchetto Maven LiteRT alle dipendenze build.gradle:

dependencies {
  ...
  implementation("com.google.ai.edge.litert:litert:+")
}

Integrazione del runtime

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

Esegui in C++ multipiattaforma

Consulta l'esempio di implementazione nell' app C++ per la segmentazione asincrona.

Dipendenze build Bazel

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

  • Libreria condivisa dell'API C LiteRT: l'attributo data deve includere la libreria condivisa dell'API C LiteRT (//litert/c:litert_runtime_c_api_shared_lib) e l'oggetto condiviso di invio specifico del fornitore per la NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so).
  • Librerie di backend specifiche della 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ò trasferire i calcoli sulla NPU.
  • Dipendenze degli attributi: l'attributo deps si collega alle dipendenze di compilazione essenziali, come il buffer tensore di LiteRT (//litert/cc:litert_tensor_buffer) e l'API per il livello di invio della NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). In questo modo, il codice dell'applicazione può interagire con la NPU tramite LiteRT.
  • File di modelli e altri asset: inclusi tramite l'attributo data.

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

Configura un ambiente NPU

Alcuni backend NPU richiedono librerie o dipendenze di runtime. Quando utilizzi l'API del 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 del runtime

Il seguente snippet di codice mostra un'implementazione di base dell'intero processo 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 di zero-copy consente a una NPU di accedere ai dati direttamente nella propria memoria senza che la CPU debba copiarli esplicitamente. Non copiando i dati nella memoria della CPU, zero-copy può ridurre significativamente la latenza end-to-end.

Il seguente codice è un esempio di implementazione 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();

Concatena più inferenze NPU

Per pipeline complesse, puoi concatenare più inferenze 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 on-device della NPU

LiteRT supporta la compilazione on-device (nota come JIT) della NPU dei modelli .tflite. La compilazione JIT può essere particolarmente utile nelle situazioni in cui non è possibile compilare il modello in anticipo.

Tuttavia, la compilazione JIT può comportare un overhead di latenza e memoria per tradurre il modello fornito dall'utente in istruzioni bytecode NPU on demand. Per ridurre al minimo l'impatto sulle prestazioni, è possibile memorizzare nella cache gli artefatti di compilazione NPU.

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 abilitare la memorizzazione nella cache della compilazione 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 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 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 Inizializzazione del modello con compilazione NPU Inizializzazione del modello con compilazione memorizzata nella cache Footprint della memoria di inizializzazione con compilazione NPU Memoria di inizializzazione con compilazione memorizzata nella cache
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 Inizializzazione del modello con compilazione NPU Inizializzazione del modello con compilazione memorizzata nella cache Footprint della memoria di inizializzazione con compilazione NPU Memoria di inizializzazione con compilazione memorizzata nella cache
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