Exécuter LiteRT Next sur Android avec C++

Les API LiteRT Next sont disponibles en C++ et peuvent offrir aux développeurs Android un meilleur contrôle de l'allocation de mémoire et du développement de bas niveau que les API Kotlin.

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

Premiers pas

Pour ajouter LiteRT Next à votre application Android, procédez comme suit :

Mettre à jour la configuration de compilation

La création d'une application C++ avec LiteRT pour l'accélération du GPU, du NPU et du processeur à l'aide de Bazel implique de définir une règle cc_binary pour s'assurer que tous les composants nécessaires sont compilés, lié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:règle Bazel fondamentale utilisée pour définir votre cible d'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 empaqueter les bibliothèques partagées et les éléments que votre application charge au moment de l'exécution.
    • LiteRT Core Runtime:bibliothèque partagée principale de l'API C LiteRT (par exemple, //litert/c:litert_runtime_c_api_shared_lib).
    • Bibliothèques de distribution: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 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 du 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 de modèle:fichiers de modèle entraînés, images de test, nuanceurs ou toute autre donnée nécessaire au moment de l'exécution (par exemple, :model_files, :shader_files).
  • Attribut deps (dépendances au moment de la compilation) : liste les bibliothèques avec lesquelles votre code doit être compilé.
    • API et utilitaires LiteRT:en-têtes et bibliothèques statiques pour les composants LiteRT tels que les tampons de tenseur (par exemple, //litert/cc:litert_tensor_buffer).
    • Bibliothèques graphiques (pour 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'outil d'association, qui peuvent inclure l'association à des bibliothèques système (par exemple, -landroid pour les builds 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 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. 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 l'environnement d'exécution 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 stocker les données d'entrée que vous allez fournir au modèle pour l'inférence et les données de sortie que le modèle produit 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 des données directement dans le premier tampon d'entrée.

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

Appeler le modèle

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

compiled_model.Run(input_buffers, output_buffers);

Récupérer les sorties

Récupérer 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 LiteRT Next.

Gestion des erreurs

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

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 génère pas d'erreur, sinon il renvoie l'erreur.

    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) fonctionne de la même manière que LITERT_ASSIGN_OR_RETURN, mais interrompt le programme en cas d'erreur.

  • LITERT_RETURN_IF_ERROR(expr) renvoie expr si son évaluation génère une erreur.

  • LITERT_ABORT_IF_ERROR(expr) fonctionne de la même manière 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 de modèle compilé (CompiledModel) est chargée de charger un modèle, d'appliquer l'accélération matérielle, d'instancier l'environnement d'exécution, 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, puis 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.

Tampon de tensor (TensorBuffer)

LiteRT Next est compatible avec l'interopérabilité des tampons d'E/S, à l'aide de l'API Tensor Buffer (TensorBuffer) pour gérer le flux de données vers et depuis le 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 d'entrée/sortie du modèle de requête

Les exigences d'allocation d'un tampon de tenseur (TensorBuffer) sont généralement spécifiées par l'accélérateur matériel. Les tampons d'entrée et de sortie peuvent avoir des exigences concernant l'alignement, les pas 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 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 tampons de tensor gérés (TensorBuffers)

L'extrait de code simplifié suivant montre comment créer des tampons de tenseur gérés, 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 tampons de tensor sans copie

Pour encapsuler un tampon existant en tant que tampon de tenseur (copie nulle), 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 tampon de tensor

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émoires tampons matérielles spécialisées

Certains types de tampons, tels que AHardwareBuffer, permettent l'interopérabilité avec d'autres types de tampons. 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, des 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

Reportez-vous aux implémentations suivantes de LiteRT Next 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 Next.

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

Copier sans copie avec la mémoire hôte

L'API LiteRT Next Compiled Model réduit les frictions des pipelines d'inférence, en particulier lorsque vous travaillez avec plusieurs backends matériels et des 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);