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) sans vous demander de parcourir les compilateurs, les runtimes ou les dépendances de bibliothèque spécifiques aux fournisseurs. L'utilisation de LiteRT pour l'accélération NPU améliore les performances pour l'inférence en temps réel et les grands modèles, et minimise les copies de mémoire grâce à l'utilisation de tampons matériels sans copie.

Premiers pas

Fournisseurs de NPU

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

Qualcomm AI Engine Direct

MediaTek NeuroPilot

Google Tensor

Le SDK Google Tensor est en accès expérimental. inscrivez-vous.

Compilation AOT et sur l'appareil

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

  • Compilation hors connexion (AOT) : cette méthode est idéale pour les modèles volumineux et complexes dont le SoC cible est connu. La compilation AOT 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. Cette option est idéale pour la distribution de petits modèles indépendamment 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.

Le guide suivant explique comment déployer la compilation AOT et sur l'appareil en trois étapes.

Étape 1 : Compilation AOT pour les SoC NPU cibles

Vous pouvez utiliser le compilateur LiteRT AOT (Ahead-Of-Time) pour compiler votre modèle .tflite sur les SoC compatibles. Vous pouvez également cibler plusieurs fournisseurs et versions de SoC simultanément dans un même processus de compilation. Pour en savoir plus, consultez le notebook sur la compilation AOT LiteRT. 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éployez l'application avec Google Play si vous êtes sur Android

Sur Android, utilisez Play for On-device AI (PODAI) de Google pour déployer les bibliothèques d'exécution de modèles et de 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 Play AI Pack Google. Vous importez ensuite le pack d'IA sur Google Play pour fournir automatiquement les modèles compilés appropriés aux 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 aux 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 des packs d'IA Play.

Ajouter le pack d'IA au projet

Importez les packs d'IA dans le projet Gradle en les copiant 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 à partir 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 le pack d'IA pour la distribution à la demande

La diffusion à 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. Avec 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 diffusion au moment de l'installation et la diffusion rapide à partir de 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,
)

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 de téléchargement initiale, 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 diffusion au moment de l'installation.

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

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

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

Exécutez le script pour télécharger les bibliothèques compatibles avec l'unité de traitement neuronal. 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 à partir 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: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: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 le NPU à l'aide de LiteRT Runtime

LiteRT élimine la complexité du développement pour 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 robuste et intégré : vous pouvez spécifier le CPU, 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 fallback, ce qui est pratique. Il fournit une délégation partielle sur l'unité de traitement neuronal (NPU), où les sous-graphiques non compatibles s'exécutent de manière fluide sur le processeur ou le GPU, selon les spécifications.

Exécuter en Kotlin

Consultez l'exemple d'implémentation dans les applications de démonstration suivantes :

Ajouter des dépendances Android

Vous pouvez ajouter le dernier package Maven LiteRT à 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

Consultez l'exemple d'implémentation dans 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 d'application principale (par exemple, main.cc) nécessite les composants d'exécution suivants :

  • Bibliothèque partagée de l'API LiteRT C : l'attribut data 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 l'unité de traitement neuronal (//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 le runtime LiteRT peut décharger les calculs sur le NPU.
  • Dépendances des attributs : l'attribut deps établit un lien avec les dépendances essentielles au moment de la compilation, telles que le tampon de Tensor de LiteRT (//litert/cc:litert_tensor_buffer) et l'API pour la couche de répartition de l'unité de traitement neuronal (//litert/vendors/qualcomm/dispatch:dispatch_api). Cela permet au code de votre application d'interagir avec l'unité de traitement neuronal via LiteRT.
  • Fichiers de modèle et autres composants : inclus via l'attribut data.

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

Configurer un environnement NPU

Certains backends NPU nécessitent des bibliothèques ou des dépendances d'exécution. Lorsque vous utilisez l'API de modèle compilé, LiteRT organise ces exigences à l'aide d'un objet Environment. Utilisez le code suivant pour trouver les pilotes ou bibliothèques 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 montre 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 à une NPU d'accéder directement aux données dans sa propre mémoire sans que le CPU ait besoin de copier explicitement ces données. En évitant de copier 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 l'unité de traitement neuronal (NPU) sans copie avec AHardwareBuffer, qui transmet les données directement à la NPU. Cette implémentation évite les coûteux allers-retours 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 enchaî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 l'unité de traitement neuronal :

// 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 NPU sur l'appareil

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

Toutefois, la compilation JIT peut entraîner une certaine latence et une surcharge de mémoire pour traduire le modèle fourni par l'utilisateur en instructions de bytecode NPU à la demande. Pour minimiser l'impact sur les performances, les artefacts de compilation de l'unité de traitement neuronal 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 du compilateur NPU du fournisseur a changé.
  • L'empreinte numérique de la version Android a changé.
  • Le modèle fourni par l'utilisateur a été modifié.
  • Les options de compilation ont été modifiées.

Pour activer la mise en cache de la compilation NPU, spécifiez le tag d'environnement CompilerCacheDir dans les options d'environnement. La valeur doit être définie sur un chemin d'accès inscriptible existant 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 de l'unité de traitement neuronal (NPU) peuvent varier en fonction de plusieurs facteurs, comme le chip NPU sous-jacent, la complexité du modèle d'entrée, etc.

Le tableau suivant compare le temps d'initialisation de l'exécution et la consommation de mémoire lorsque la compilation de l'unité de traitement neuronal (NPU) est requise par rapport au cas où elle 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 la compilation mise en cache ; Empreinte mémoire d'initialisation avec compilation NPU Mémoire init avec compilation mise en cache
torchvision_resnet152.tflite 7465.22 ms 198,34 ms 1525,24 MB 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 informations suivantes :

Modèle TFLite Initialisation du modèle avec compilation NPU initialisation du modèle avec la compilation mise en cache ; Empreinte mémoire d'initialisation avec compilation NPU Mémoire init avec compilation mise en cache
torchvision_resnet152.tflite 2766.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