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 vous obliger à parcourir les compilateurs, les environnements d'exécution 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

Pour commencer, consultez le guide de présentation des NPU :

Pour obtenir des exemples d'implémentations de LiteRT avec prise en charge de l'unité de traitement neuronal (NPU), consultez les applications de démonstration suivantes :

Fournisseurs de NPU

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

Qualcomm AI Engine Direct

  • Les chemins d'exécution de la compilation AOT et sur l'appareil sont compatibles avec l'API Compiled Model.
  • Pour en savoir plus sur la configuration, consultez Qualcomm AI Engine Direct.

MediaTek NeuroPilot

  • Les chemins d'exécution AOT et JIT sont compatibles avec l'API Compiled Model.
  • Pour en savoir plus sur la configuration, consultez MediaTek NeuroPilot.

Convertir et compiler des modèles pour l'unité de traitement neuronal

Pour utiliser l'accélération NPU avec LiteRT, les modèles doivent être convertis au format de fichier LiteRT et compilés pour une utilisation NPU sur l'appareil. Vous pouvez utiliser le compilateur LiteRT AOT (Ahead-Of-Time) pour compiler des modèles dans un package d'IA, qui regroupe vos modèles compilés avec des configurations de ciblage d'appareils. Cela permet de vérifier que les modèles sont correctement diffusés sur les appareils, selon qu'ils sont équipés ou optimisés pour des SoC spécifiques.

Après avoir converti et compilé les modèles, vous pouvez utiliser Play for On-device AI (PODAI) pour importer des modèles sur Google Play et les distribuer aux appareils via le framework On-Demand AI.

Consultez le notebook de compilation AOT LiteRT pour obtenir un guide complet sur la conversion et la compilation de modèles pour les NPU.

[AOT uniquement] Déployer avec Play AI Pack

Après avoir converti le modèle et compilé un package d'IA, suivez les étapes ci-dessous pour déployer le package d'IA avec Google Play.

Importer des packs d'IA dans le projet Gradle

Copiez 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 bibliothèques d'exécution NPU au projet

Téléchargez litert_npu_runtime_libraries.zip pour AOT ou litert_npu_runtime_libraries_jit.zip pour JIT, 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 packs d'IA et 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

...

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

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

  // [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 uniquement] Utiliser le déploiement à la demande

Une fois la fonctionnalité Android AI Pack configurée dans le fichier build.gradle.kts, vérifiez les capacités de l'appareil et utilisez le NPU sur les appareils compatibles, en utilisant le GPU et le CPU comme solution de secours :

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

Créer CompiledModel pour le mode JIT

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

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

Inférence sur le NPU à l'aide de LiteRT en Kotlin

Pour commencer à utiliser l'accélérateur NPU, transmettez le paramètre NPU lors de la création du modèle compilé (CompiledModel).

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

Inférence sur le NPU à l'aide de LiteRT en C++

Créer des dépendances

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 dépendances ou des bibliothèques 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));

Aucune 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 processeur n'ait besoin de les copier explicitement. 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 juste-à-temps de l'unité de traitement neuronal

LiteRT est compatible avec la compilation JIT (just-in-time) des modèles .tflite par le NPU. 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 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 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 changé.
  • 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 est requise par rapport au cas 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 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 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 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