Exécuter l'API LiteRT Compiled Model sur Android avec C++

Les API LiteRT Compiled Model sont disponibles en C++, ce qui permet aux développeurs Android de contrôler précisément l'allocation de mémoire et le développement de bas niveau.

Pour obtenir un exemple d'application LiteRT en C++, consultez la démonstration de la segmentation asynchrone avec C++.

Premiers pas

Suivez les étapes ci-dessous pour ajouter l'API LiteRT Compiled Model à votre application Android.

Mettre à jour la configuration de compilation

Pour créer une application C++ avec LiteRT pour l'accélération GPU, NPU et CPU à l'aide de Bazel, vous devez définir une règle cc_binary afin de vous assurer que tous les composants nécessaires sont compilés, associés et empaquetés. L'exemple de configuration suivant permet à votre application de choisir ou d'utiliser dynamiquement des accélérateurs GPU, NPU et CPU.

Voici les principaux composants de votre configuration de compilation Bazel :

  • Règle cc_binary : il s'agit de la règle Bazel fondamentale utilisée pour définir votre cible exécutable C++ (par exemple, name = "your_application_name").
  • Attribut srcs : liste les fichiers sources C++ de votre application (par exemple, main.cc et d'autres fichiers .cc ou .h).
  • Attribut data (dépendances d'exécution) : cet attribut est essentiel pour regrouper les bibliothèques partagées et les composants que votre application charge au moment de l'exécution.
    • Runtime Core LiteRT : bibliothèque partagée de l'API C LiteRT principale (par exemple, //litert/c:litert_runtime_c_api_shared_lib).
    • Bibliothèques Dispatch : bibliothèques partagées spécifiques au fournisseur que LiteRT utilise pour communiquer avec les pilotes matériels (par exemple, //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • Bibliothèques de backend GPU : bibliothèques partagées pour l'accélération GPU (par exemple, "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Bibliothèques de backend NPU : bibliothèques partagées spécifiques à l'accélération NPU, telles que les bibliothèques QNN HTP de Qualcomm (par exemple, @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • Fichiers et éléments du modèle : fichiers de modèle entraînés, images de test, nuanceurs ou toute autre donnée nécessaire à l'exécution (par exemple, :model_files, :shader_files).
  • Attribut deps (dépendances au moment de la compilation) : liste les bibliothèques dont votre code a besoin pour la compilation.
    • API et utilitaires LiteRT : en-têtes et bibliothèques statiques pour les composants LiteRT tels que les tampons de Tensor (par exemple, //litert/cc:litert_tensor_buffer).
    • Bibliothèques graphiques (pour le GPU) : dépendances liées aux API graphiques si l'accélérateur GPU les utilise (par exemple, gles_deps()).
  • Attribut linkopts : spécifie les options transmises à l'éditeur de liens, qui peuvent inclure l'association à des bibliothèques système (par exemple, -landroid pour les versions Android ou les bibliothèques GLES avec gles_linkopts()).

Voici un exemple de règle cc_binary :

cc_binary(
    name = "your_application",
    srcs = [
        "main.cc",
    ],
    data = [
        ...
        # litert c api shared library
        "//litert/c:litert_runtime_c_api_shared_lib",
        # GPU accelerator shared library
        "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so",
        # NPU accelerator shared library
        "//litert/vendors/qualcomm/dispatch:dispatch_api_so",
    ],
    linkopts = select({
        "@org_tensorflow//tensorflow:android": ["-landroid"],
        "//conditions:default": [],
    }) + gles_linkopts(), # gles link options
    deps = [
        ...
        "//litert/cc:litert_tensor_buffer", # litert cc library
        ...
    ] + gles_deps(), # gles dependencies
)

Charger le modèle

Après avoir obtenu un modèle LiteRT ou converti un modèle au format .tflite, chargez le modèle en créant un objet Model.

LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));

Créer l'environnement

L'objet Environment fournit un environnement d'exécution qui inclut des composants tels que le chemin d'accès du plug-in du compilateur et les contextes GPU. Le Environment est obligatoire lors de la création de CompiledModel et TensorBuffer. Le code suivant crée un Environment pour l'exécution du processeur et du GPU sans aucune option :

LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));

Créer le modèle compilé

À l'aide de l'API CompiledModel, initialisez le runtime avec l'objet Model nouvellement créé. Vous pouvez spécifier l'accélération matérielle à ce stade (kLiteRtHwAcceleratorCpu ou kLiteRtHwAcceleratorGpu) :

LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, kLiteRtHwAcceleratorCpu));

Créer des tampons d'entrée et de sortie

Créez les structures de données (tampons) nécessaires pour contenir les données d'entrée que vous fournirez au modèle pour l'inférence, ainsi que les données de sortie que le modèle produira après l'exécution de l'inférence.

LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

Si vous utilisez la mémoire du processeur, remplissez les entrées en écrivant directement les données dans le premier tampon d'entrée.

input_buffers[0].Write<float>(absl::MakeConstSpan(input_data, input_size));

Appeler le modèle

Fournissez les tampons d'entrée et de sortie, puis exécutez le modèle compilé avec le modèle et l'accélération matérielle spécifiés dans les étapes précédentes.

compiled_model.Run(input_buffers, output_buffers);

Récupérer les sorties

Récupérez les sorties en lisant directement la sortie du modèle à partir de la mémoire.

std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));
// ... process output data

Concepts et composants clés

Consultez les sections suivantes pour en savoir plus sur les concepts et composants clés des API de modèle compilé LiteRT.

Gestion des erreurs

LiteRT utilise litert::Expected pour renvoyer des valeurs ou propager des erreurs de manière semblable à absl::StatusOr ou std::expected. Vous pouvez vérifier manuellement si l'erreur s'est produite.

Pour plus de commodité, LiteRT fournit les macros suivantes :

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) attribue le résultat de expr à lhs s'il ne produit pas d'erreur, et renvoie l'erreur dans le cas contraire.

    Il se développera pour ressembler à l'extrait suivant.

    auto maybe_model = Model::CreateFromFile("mymodel.tflite");
    if (!maybe_model) {
      return maybe_model.Error();
    }
    auto model = std::move(maybe_model.Value());
    
  • LITERT_ASSIGN_OR_ABORT(lhs, expr) effectue la même opération que LITERT_ASSIGN_OR_RETURN, mais interrompt le programme en cas d'erreur.

  • LITERT_RETURN_IF_ERROR(expr) renvoie expr si son évaluation produit une erreur.

  • LITERT_ABORT_IF_ERROR(expr) fait la même chose que LITERT_RETURN_IF_ERROR, mais interrompt le programme en cas d'erreur.

Pour en savoir plus sur les macros LiteRT, consultez litert_macros.h.

Modèle compilé (CompiledModel)

L'API Compiled Model (CompiledModel) est chargée de charger un modèle, d'appliquer l'accélération matérielle, d'instancier le runtime, de créer des tampons d'entrée et de sortie, et d'exécuter l'inférence.

L'extrait de code simplifié suivant montre comment l'API Compiled Model prend un modèle LiteRT (.tflite) et l'accélérateur matériel cible (GPU), puis crée un modèle compilé prêt à exécuter l'inférence.

// Load model and initialize runtime
LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, kLiteRtHwAcceleratorCpu));

L'extrait de code simplifié suivant montre comment l'API Compiled Model prend un tampon d'entrée et de sortie, et exécute des inférences avec le modèle compilé.

// Preallocate input/output buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// Fill the first input
float input_values[] = { /* your data */ };
LITERT_RETURN_IF_ERROR(
  input_buffers[0].Write<float>(absl::MakeConstSpan(input_values, /*size*/)));

// Invoke
LITERT_RETURN_IF_ERROR(compiled_model.Run(input_buffers, output_buffers));

// Read the output
std::vector<float> data(output_data_size);
LITERT_RETURN_IF_ERROR(
  output_buffers[0].Read<float>(absl::MakeSpan(data)));

Pour obtenir une vue plus complète de l'implémentation de l'API CompiledModel, consultez le code source de litert_compiled_model.h.

TensorBuffer

LiteRT offre une compatibilité intégrée pour l'interopérabilité des tampons d'E/S, en utilisant l'API Tensor Buffer (TensorBuffer) pour gérer le flux de données entrant et sortant du modèle compilé. L'API Tensor Buffer permet d'écrire (Write<T>()) et de lire (Read<T>()), et de verrouiller la mémoire du processeur.

Pour obtenir une vue plus complète de l'implémentation de l'API TensorBuffer, consultez le code source de litert_tensor_buffer.h.

Exigences concernant les entrées et les sorties du modèle de requête

Les exigences pour allouer un Tensor Buffer (TensorBuffer) sont généralement spécifiées par l'accélérateur matériel. Les tampons d'entrées et de sorties peuvent avoir des exigences concernant l'alignement, les foulées de tampon et le type de mémoire. Vous pouvez utiliser des fonctions d'assistance telles que CreateInputBuffers pour gérer automatiquement ces exigences.

L'extrait de code simplifié suivant montre comment récupérer les exigences de mémoire tampon pour les données d'entrée :

LITERT_ASSIGN_OR_RETURN(auto reqs, compiled_model.GetInputBufferRequirements(signature_index, input_index));

Pour obtenir une vue plus complète de l'implémentation de l'API TensorBufferRequirements, consultez le code source de litert_tensor_buffer_requirements.h.

Créer des TensorBuffers gérés

L'extrait de code simplifié suivant montre comment créer des Managed Tensor Buffers, où l'API TensorBuffer alloue les tampons respectifs :

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_cpu,
TensorBuffer::CreateManaged(env, /*buffer_type=*/kLiteRtTensorBufferTypeHostMemory,
  ranked_tensor_type, buffer_size));

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_gl, TensorBuffer::CreateManaged(env,
  /*buffer_type=*/kLiteRtTensorBufferTypeGlBuffer, ranked_tensor_type, buffer_size));

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_ahwb, TensorBuffer::CreateManaged(env,
  /*buffer_type=*/kLiteRtTensorBufferTypeAhwb, ranked_tensor_type, buffer_size));

Créer des Tensor Buffers sans copie

Pour encapsuler un tampon existant en tant que Tensor Buffer (sans copie), utilisez l'extrait de code suivant :

// Create a TensorBuffer from host memory
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_host,
  TensorBuffer::CreateFromHostMemory(env, ranked_tensor_type,
  ptr_to_host_memory, buffer_size));

// Create a TensorBuffer from GlBuffer
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_gl,
  TensorBuffer::CreateFromGlBuffer(env, ranked_tensor_type, gl_target, gl_id,
  size_bytes, offset));

// Create a TensorBuffer from AHardware Buffer
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_ahwb,
  TensorBuffer::CreateFromAhwb(env, ranked_tensor_type, ahardware_buffer, offset));

Lire et écrire à partir du Tensor Buffer

L'extrait de code suivant montre comment lire à partir d'un tampon d'entrée et écrire dans un tampon de sortie :

// Example of reading to input buffer:
std::vector<float> input_tensor_data = {1,2};
LITERT_ASSIGN_OR_RETURN(auto write_success,
  input_tensor_buffer.Write<float>(absl::MakeConstSpan(input_tensor_data)));
if(write_success){
  /* Continue after successful write... */
}

// Example of writing to output buffer:
std::vector<float> data(total_elements);
LITERT_ASSIGN_OR_RETURN(auto read_success,
  output_tensor_buffer.Read<float>(absl::MakeSpan(data)));
if(read_success){
  /* Continue after successful read */
}

Avancé : Interopérabilité de la mémoire tampon sans copie pour les types de mémoire tampon matériels spécialisés

Certains types de tampon, tels que AHardwareBuffer, permettent l'interopérabilité avec d'autres types de tampon. Par exemple, un tampon OpenGL peut être créé à partir d'un AHardwareBuffer sans copie. L'extrait de code suivant en est un exemple :

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_ahwb,
  TensorBuffer::CreateManaged(env, kLiteRtTensorBufferTypeAhwb,
  ranked_tensor_type, buffer_size));
// Buffer interop: Get OpenGL buffer from AHWB,
// internally creating an OpenGL buffer backed by AHWB memory.
LITERT_ASSIGN_OR_RETURN(auto gl_buffer, tensor_buffer_ahwb.GetGlBuffer());

Les tampons OpenCL peuvent également être créés à partir de AHardwareBuffer :

LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_ahwb.GetOpenClMemory());

Sur les appareils mobiles compatibles avec l'interopérabilité entre OpenCL et OpenGL, les tampons CL peuvent être créés à partir de tampons GL :

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_gl,
  TensorBuffer::CreateFromGlBuffer(env, ranked_tensor_type, gl_target, gl_id,
  size_bytes, offset));

// Creates an OpenCL buffer from the OpenGL buffer, zero-copy.
LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_from_gl.GetOpenClMemory());

Exemples de mise en œuvre

Consultez les implémentations suivantes de LiteRT en C++.

Inférence de base (CPU)

Vous trouverez ci-dessous une version condensée des extraits de code de la section Premiers pas. Il s'agit de l'implémentation la plus simple de l'inférence avec LiteRT.

// Load model and initialize runtime
LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model, CompiledModel::Create(env, model,
  kLiteRtHwAcceleratorCpu));

// Preallocate input/output buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// Fill the first input
float input_values[] = { /* your data */ };
input_buffers[0].Write<float>(absl::MakeConstSpan(input_values, /*size*/));

// Invoke
compiled_model.Run(input_buffers, output_buffers);

// Read the output
std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));

Zéro copie avec la mémoire hôte

L'API LiteRT Compiled Model réduit les frictions des pipelines d'inférence, en particulier lorsqu'il s'agit de plusieurs backends matériels et de flux sans copie. L'extrait de code suivant utilise la méthode CreateFromHostMemory lors de la création du tampon d'entrée, qui utilise la copie zéro avec la mémoire hôte.

// Define an LiteRT environment to use existing EGL display and context.
const std::vector<Environment::Option> environment_options = {
   {OptionTag::EglDisplay, user_egl_display},
   {OptionTag::EglContext, user_egl_context}};
LITERT_ASSIGN_OR_RETURN(auto env,
   Environment::Create(absl::MakeConstSpan(environment_options)));

// Load model1 and initialize runtime.
LITERT_ASSIGN_OR_RETURN(auto model1, Model::CreateFromFile("model1.tflite"));
LITERT_ASSIGN_OR_RETURN(auto compiled_model1, CompiledModel::Create(env, model1, kLiteRtHwAcceleratorGpu));

// Prepare I/O buffers. opengl_buffer is given outside from the producer.
LITERT_ASSIGN_OR_RETURN(auto tensor_type, model.GetInputTensorType("input_name0"));
// Create an input TensorBuffer based on tensor_type that wraps the given OpenGL Buffer.
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_opengl,
    litert::TensorBuffer::CreateFromGlBuffer(env, tensor_type, opengl_buffer));

// Create an input event and attach it to the input buffer. Internally, it creates
// and inserts a fence sync object into the current EGL command queue.
LITERT_ASSIGN_OR_RETURN(auto input_event, Event::CreateManaged(env, LiteRtEventTypeEglSyncFence));
tensor_buffer_from_opengl.SetEvent(std::move(input_event));

std::vector<TensorBuffer> input_buffers;
input_buffers.push_back(std::move(tensor_buffer_from_opengl));

// Create an output TensorBuffer of the model1. It's also used as an input of the model2.
LITERT_ASSIGN_OR_RETURN(auto intermedidate_buffers,  compiled_model1.CreateOutputBuffers());

// Load model2 and initialize runtime.
LITERT_ASSIGN_OR_RETURN(auto model2, Model::CreateFromFile("model2.tflite"));
LITERT_ASSIGN_OR_RETURN(auto compiled_model2, CompiledModel::Create(env, model2, kLiteRtHwAcceleratorGpu));
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model2.CreateOutputBuffers());

compiled_model1.RunAsync(input_buffers, intermedidate_buffers);
compiled_model2.RunAsync(intermedidate_buffers, output_buffers);