Accélération de l'unité de traitement neuronal avec LiteRT

LiteRT fournit une interface unifiée pour utiliser les unités de traitement neuronal (NPU, Neural Processing Units) sans avoir à parcourir des compilateurs, des environnements d'exécution ou des dépendances de bibliothèque spécifiques à un fournisseur. L'utilisation de LiteRT pour l'accélération NPU améliore les performances pour l'inférence en temps réel et les modèles volumineux, et minimise les copies de mémoire grâce à l'utilisation de tampons matériels sans copie.

Premiers pas

Modèles de ML classiques

Pour les modèles de ML classiques, consultez les applications de démonstration suivantes.

Modèles d'IA générative

Pour les modèles d'IA générative, consultez les démonstrations et le guide suivants :

Fournisseurs de NPU

LiteRT est compatible avec l'accélération NPU des fournisseurs suivants :

Google Tensor

  • Prise en charge de l'exécution AOT via l'API CompiledModel
  • Pour en savoir plus sur la configuration, consultez Google Tensor.

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Intel OpenVino

  • Prise en charge de l'exécution de la compilation AOT et sur l'appareil via l'API CompiledModel
  • Pour en savoir plus sur la configuration, consultez Intel OpenVino.

Compilation AOT et sur l'appareil

LiteRT NPU est compatible avec la compilation AOT et sur l'appareil pour répondre à vos exigences de déploiement spécifiques :

  • Compilation hors connexion (AOT) : elle est idéale pour les modèles volumineux et complexes dont le SoC cible est connu. La compilation anticipée réduit considérablement les coûts d'initialisation et l'utilisation de la mémoire lorsque l'utilisateur lance votre application.
  • Compilation en ligne (sur l'appareil) : également appelée compilation JIT. Elle est idéale pour la distribution de petits modèles indépendants de la plate-forme. Le modèle est compilé sur l'appareil de l'utilisateur lors de l'initialisation, ce qui ne nécessite aucune étape de préparation supplémentaire, mais entraîne un coût plus élevé lors de la première exécution.

Voici comment déployer votre modèle à l'aide des options de compilation AOT ou sur l'appareil :

Étape 1 : Compilation AOT pour les SoC NPU cibles

Vous pouvez utiliser le compilateur LiteRT AOT (anticipé) pour compiler votre modèle .tflite sur les SoC compatibles. Vous pouvez également cibler simultanément plusieurs fournisseurs et versions de SoC dans un seul processus de compilation. Pour en savoir plus, consultez ce notebook de compilation LiteRT AOT. Bien que facultative, la compilation AOT est fortement recommandée pour les modèles plus volumineux afin de réduire le temps d'initialisation sur l'appareil. Cette étape n'est pas requise pour la compilation sur l'appareil.

Étape 2 : Déployer avec Google Play sur Android

Sur Android, utilisez Google Play pour l'IA sur l'appareil (PODAI) afin de déployer le modèle et les bibliothèques d'exécution NPU avec votre application.

  • Pour les modèles de compilation sur l'appareil : ajoutez le fichier de modèle .tflite d'origine directement dans le répertoire assets/ de votre application.
  • Pour les modèles de compilation AOT : utilisez LiteRT pour exporter vos modèles compilés dans un seul pack d'IA Google Play. Vous pouvez ensuite importer le pack d'IA dans Google Play pour diffuser automatiquement les modèles compilés corrects sur les appareils des utilisateurs.
  • Pour les bibliothèques d'exécution NPU, utilisez Play Feature Delivery pour distribuer les bibliothèques d'exécution appropriées sur les appareils des utilisateurs.

Consultez les sections suivantes pour savoir comment déployer avec Play AI Pack et Play Feature Delivery.

Déployer des modèles AOT avec Play AI Pack

Les étapes suivantes vous guident dans le déploiement de vos modèles compilés AOT à l'aide de Play AI Packs.

Ajouter un pack d'IA au projet

Importez des packs d'IA dans le projet Gradle en copiant le ou les packs d'IA dans le répertoire racine du projet Gradle. Exemple :

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

Ajoutez chaque pack d'IA à la configuration de compilation 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

Ajouter des packs d'IA à la configuration Gradle

Copiez device_targeting_configuration.xml des packs d'IA générés dans le répertoire du module d'application principal. Mettez ensuite à jour settings.gradle.kts :

// my_app/setting.gradle.kts

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

Mettez à jour 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")
}

Configurer un pack d'IA pour la distribution à la demande

La distribution à la demande vous permet de demander le modèle au moment de l'exécution, ce qui est utile si le modèle n'est requis que pour certains flux utilisateur. Votre modèle sera téléchargé dans l'espace de stockage interne de votre application. Une fois la fonctionnalité Android AI Pack configurée dans le fichier build.gradle.kts, vérifiez les capacités de l'appareil. Consultez également les instructions pour la distribution au moment de l'installation et la distribution rapide à partir de 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,
)

Déployer des bibliothèques d'exécution NPU avec Play Feature Delivery

Play Feature Delivery est compatible avec plusieurs options de distribution pour optimiser la taille du téléchargement initial, y compris la distribution au moment de l'installation, la distribution à la demande, la distribution conditionnelle et la distribution instantanée. Nous présentons ici le guide de base pour la distribution au moment de l'installation.

Ajouter des bibliothèques d'exécution NPU au projet

Téléchargez litert_npu_runtime_libraries.zip à partir de la dernière version pour la compilation AOT ou litert_npu_runtime_libraries_jit.zip à partir de la dernière version pour la compilation sur l'appareil, puis décompressez-le dans le répertoire racine du projet :

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

Exécutez le script pour télécharger les bibliothèques de prise en charge NPU. Par exemple, exécutez la commande suivante pour les NPU Qualcomm :

$ ./litert_npu_runtime_libraries/fetch_qualcomm_library.sh

Ajouter des bibliothèques d'exécution NPU à la configuration Gradle

Copiez device_targeting_configuration.xml des packs d'IA générés dans le répertoire du module d'application principal. Mettez ensuite à jour 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")

Mettez à jour 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"))
  ...
}

Étape 3 : Inférence sur NPU à l'aide de LiteRT Runtime

LiteRT élimine la complexité du développement par rapport à des versions spécifiques de SoC, ce qui vous permet d'exécuter votre modèle sur le NPU en quelques lignes de code. Il fournit également un mécanisme de secours intégré et robuste : vous pouvez spécifier le processeur, le GPU ou les deux comme options, et LiteRT les utilisera automatiquement si le NPU n'est pas disponible. La compilation AOT prend également en charge le secours. Elle fournit une délégation partielle sur le NPU où les sous-graphes non compatibles s'exécutent de manière transparente sur le processeur ou le GPU, comme spécifié.

Exécuter en Kotlin

Pour obtenir un exemple d'implémentation, consultez les applications de démonstration suivantes :

Ajouter des dépendances Android

Vous pouvez ajouter le dernier package LiteRT Maven à vos dépendances build.gradle :

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

Intégration de l'environnement d'exécution

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

Exécuter en C++ multiplate-forme

Pour obtenir un exemple d'implémentation, consultez l' application C++ de segmentation asynchrone.

Dépendances de compilation Bazel

Les utilisateurs de C++ doivent créer les dépendances de l'application avec l'accélération LiteRT NPU. La règle cc_binary qui regroupe la logique de l'application principale (par exemple, main.cc) nécessite les composants d'exécution suivants :

  • Bibliothèque partagée de l'API LiteRT C : l'data attribut doit inclure la bibliothèque partagée de l'API LiteRT C (//litert/c:litert_runtime_c_api_shared_lib) et l'objet partagé de répartition spécifique au fournisseur pour le NPU (//litert/vendors/qualcomm/dispatch:dispatch_api_so).
  • Bibliothèques de backend spécifiques au NPU : par exemple, les bibliothèques Qualcomm AI RT (QAIRT) pour l'hôte Android (comme libQnnHtp.so, libQnnHtpPrepare.so) et la bibliothèque Hexagon DSP correspondante (libQnnHtpV79Skel.so). Cela garantit que l'environnement d'exécution LiteRT peut décharger les calculs sur le NPU.
  • Dépendances d'attribut : l'attribut deps est lié aux dépendances essentielles au moment de la compilation, telles que le tampon de tenseur de LiteRT (//litert/cc:litert_tensor_buffer) et l'API pour la couche de répartition NPU (//litert/vendors/qualcomm/dispatch:dispatch_api). Cela permet au code de votre application d'interagir avec le NPU via LiteRT.
  • Fichiers de modèle et autres éléments : inclus via l'attribut data.

Cette configuration permet à votre binaire compilé de charger et d'utiliser dynamiquement le NPU pour l'inférence accélérée du machine learning.

Configurer un environnement NPU

Certains backends NPU nécessitent des dépendances ou des bibliothèques d'exécution. Lorsque vous utilisez l'API de modèle compilé, LiteRT organise ces exigences via un objet Environment. Utilisez le code suivant pour trouver les bibliothèques ou pilotes NPU appropriés :

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

Intégration de l'environnement d'exécution

L'extrait de code suivant présente une implémentation de base de l'ensemble du processus en 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));

Zéro copie avec accélération NPU

L'utilisation de la copie zéro permet à un NPU d'accéder directement aux données dans sa propre mémoire sans que le processeur n'ait besoin de les copier explicitement. En ne copiant pas les données vers et depuis la mémoire du processeur, la copie zéro peut réduire considérablement la latence de bout en bout.

Le code suivant est un exemple d'implémentation de NPU sans copie avec AHardwareBuffer, qui transmet les données directement au NPU. Cette implémentation évite les allers-retours coûteux vers la mémoire du processeur, ce qui réduit considérablement la surcharge d'inférence.

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

Chaîner plusieurs inférences NPU

Pour les pipelines complexes, vous pouvez chaîner plusieurs inférences NPU. Étant donné que chaque étape utilise un tampon compatible avec l'accélérateur, votre pipeline reste principalement dans la mémoire gérée par le 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);

Mise en cache de la compilation sur l'appareil NPU

LiteRT est compatible avec la compilation sur l'appareil NPU (appelée JIT) des modèles .tflite. La compilation JIT peut être particulièrement utile dans les situations où la compilation anticipée du modèle n'est pas possible.

La compilation JIT peut toutefois entraîner une certaine latence et une surcharge de mémoire pour traduire à la demande le modèle fourni par l'utilisateur en instructions de bytecode NPU. Pour minimiser l'impact sur les performances, les artefacts de compilation NPU peuvent être mis en cache.

Lorsque la mise en cache est activée, LiteRT ne déclenche la recompilation du modèle que lorsque cela est nécessaire, par exemple :

  • La version du plug-in de compilateur NPU du fournisseur a changé ;
  • L'empreinte de compilation Android a changé ;
  • Le modèle fourni par l'utilisateur a changé ;
  • Les options de compilation ont changé.

Pour activer la mise en cache de la compilation NPU, spécifiez la balise d'environnement CompilerCacheDir dans les options d'environnement. La valeur doit être définie sur un chemin d'accès existant et accessible en écriture de l'application.

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

Exemple d'économies de latence et de mémoire :

Le temps et la mémoire requis pour la compilation NPU peuvent varier en fonction de plusieurs facteurs, tels que la puce NPU sous-jacente, la complexité du modèle d'entrée, etc.

Le tableau suivant compare le temps d'initialisation de l'environnement d'exécution et la consommation de mémoire lorsque la compilation NPU est requise par rapport au moment où la compilation peut être ignorée en raison de la mise en cache. Sur un exemple d'appareil, nous obtenons les résultats suivants :

Modèle TFLite Initialisation du modèle avec compilation NPU Initialisation du modèle avec compilation mise en cache Espace mémoire utilisé initial avec compilation NPU Mémoire initiale avec compilation mise en cache
torchvision_resnet152.tflite 7 465,22 ms 198,34 ms 1 525,24 Mo 355,07 Mo
torchvision_lraspp_mobilenet_v3_large.tflite 1 592,54 ms 166,47 ms 254,90 Mo 33,78 Mo

Sur un autre appareil, nous obtenons les résultats suivants :

Modèle TFLite Initialisation du modèle avec compilation NPU Initialisation du modèle avec compilation mise en cache Espace mémoire utilisé initial avec compilation NPU Mémoire initiale avec compilation mise en cache
torchvision_resnet152.tflite 2 766,44 ms 379,86 ms 653,54 Mo 501,21 Mo
torchvision_lraspp_mobilenet_v3_large.tflite 784,14 ms 231,76 ms 113,14 Mo 67,49 Mo