Ejecuta LiteRT Next en Android con C++

Las APIs de LiteRT Next están disponibles en C++ y pueden ofrecer a los desarrolladores de Android un mayor control sobre la asignación de memoria y el desarrollo de bajo nivel que las APIs de Kotlin.

Para ver un ejemplo de una aplicación de LiteRT Next en C++, consulta la demostración de segmentación asíncrona con C++.

Comenzar

Sigue estos pasos para agregar LiteRT Next a tu aplicación para Android.

Actualiza la configuración de compilación

La compilación de una aplicación C++ con LiteRT para la aceleración de GPU, NPU y CPU con Bazel implica definir una regla cc_binary para garantizar que todos los componentes necesarios se compilen, vinculen y empaqueten. La siguiente configuración de ejemplo permite que tu aplicación elija o use de forma dinámica aceleradores de GPU, NPU y CPU.

Estos son los componentes clave de la configuración de compilación de Bazel:

  • Regla cc_binary: Esta es la regla fundamental de Bazel que se usa para definir tu objetivo ejecutable de C++ (p.ej., name = "your_application_name").
  • Atributo srcs: Muestra una lista de los archivos fuente de C++ de tu aplicación (p.ej., main.cc y otros archivos .cc o .h).
  • Atributo data (Dependencias del entorno de ejecución): Es fundamental para empaquetar bibliotecas y recursos compartidos que tu aplicación carga en el entorno de ejecución.
    • LiteRT Core Runtime: Es la biblioteca compartida principal de la API de LiteRT C (p.ej., //litert/c:litert_runtime_c_api_shared_lib).
    • Bibliotecas de envío: Son bibliotecas compartidas específicas del proveedor que LiteRT usa para comunicarse con los controladores de hardware (p.ej., //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • Bibliotecas de backend de GPU: Son las bibliotecas compartidas para la aceleración de GPU (p.ej., "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Bibliotecas de backend de NPU: Son las bibliotecas compartidas específicas para la aceleración de NPU, como las bibliotecas de HTP de QNN de Qualcomm (p.ej., @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • Archivos y recursos del modelo: Son los archivos de tu modelo entrenado, las imágenes de prueba, los sombreadores o cualquier otro dato necesario durante el tiempo de ejecución (p.ej., :model_files, :shader_files).
  • Atributo deps (Dependencias del tiempo de compilación): Aquí se enumeran las bibliotecas con las que se debe compilar tu código.
    • APIs y utilidades de LiteRT: Encabezados y bibliotecas estáticas para componentes de LiteRT, como búferes de tensores (p.ej., //litert/cc:litert_tensor_buffer).
    • Bibliotecas de gráficos (para GPU): Son dependencias relacionadas con las APIs de gráficos si el acelerador de GPU las usa (p.ej., gles_deps()).
  • Atributo linkopts: Especifica las opciones que se pasan al vinculador, que pueden incluir la vinculación con bibliotecas del sistema (p.ej., -landroid para compilaciones de Android o bibliotecas de GLES con gles_linkopts()).

El siguiente es un ejemplo de una regla 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
)

Carga el modelo

Después de obtener un modelo de LiteRT o de convertirlo al formato .tflite, carga el modelo creando un objeto Model.

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

Crea el entorno

El objeto Environment proporciona un entorno de ejecución que incluye componentes, como la ruta de acceso del complemento del compilador y los contextos de la GPU. Environment es obligatorio cuando se crean CompiledModel y TensorBuffer. El siguiente código crea un Environment para la ejecución de CPU y GPU sin ninguna opción:

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

Crea el modelo compilado

Con la API de CompiledModel, inicializa el entorno de ejecución con el objeto Model que acabas de crear. Puedes especificar la aceleración de hardware en este punto (kLiteRtHwAcceleratorCpu o kLiteRtHwAcceleratorGpu):

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

Cómo crear búferes de entrada y salida

Crea las estructuras de datos (buffers) necesarias para contener los datos de entrada que alimentarás al modelo para la inferencia y los datos de salida que el modelo produce después de ejecutar la inferencia.

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

Si usas la memoria de la CPU, escribe datos directamente en el primer búfer de entrada para completar las entradas.

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

Cómo invocar el modelo

Proporciona los búferes de entrada y salida, y ejecuta el modelo compilado con el modelo y la aceleración de hardware especificados en los pasos anteriores.

compiled_model.Run(input_buffers, output_buffers);

Cómo recuperar resultados

Recupera los resultados leyendo directamente el resultado del modelo desde la memoria.

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

Conceptos y componentes clave

Consulta las siguientes secciones para obtener información sobre los conceptos y componentes clave de las APIs de LiteRT Next.

Manejo de errores

LiteRT usa litert::Expected para mostrar valores o propagar errores de una manera similar a absl::StatusOr o std::expected. Puedes verificar el error manualmente.

Para mayor comodidad, LiteRT proporciona las siguientes macros:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) asigna el resultado de expr a lhs si no genera un error y, de lo contrario, muestra el error.

    Se expandirá a algo similar al siguiente fragmento.

    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) hace lo mismo que LITERT_ASSIGN_OR_RETURN, pero aborta el programa en caso de error.

  • LITERT_RETURN_IF_ERROR(expr) muestra expr si su evaluación genera un error.

  • LITERT_ABORT_IF_ERROR(expr) hace lo mismo que LITERT_RETURN_IF_ERROR, pero aborta el programa en caso de error.

Para obtener más información sobre las macros de LiteRT, consulta litert_macros.h.

Modelo compilado (CompiledModel)

La API de Compiled Model (CompiledModel) es responsable de cargar un modelo, aplicar la aceleración de hardware, crear instancias del entorno de ejecución, crear búferes de entrada y salida, y ejecutar la inferencia.

En el siguiente fragmento de código simplificado, se muestra cómo la API de Compiled Model toma un modelo LiteRT (.tflite) y el acelerador de hardware de destino (GPU) y crea un modelo compilado listo para ejecutar inferencias.

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

En el siguiente fragmento de código simplificado, se muestra cómo la API de Compiled Model toma un búfer de entrada y salida, y ejecuta inferencias con el modelo compilado.

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

Para obtener una vista más completa de cómo se implementa la API de CompiledModel, consulta el código fuente de litert_compiled_model.h.

Buffer de tensores (TensorBuffer)

LiteRT Next proporciona compatibilidad integrada con la interoperabilidad del búfer de E/S, con la API de Tensor Buffer (TensorBuffer) para controlar el flujo de datos dentro y fuera del modelo compilado. La API de Tensor Buffer proporciona la capacidad de escribir (Write<T>()) y leer (Read<T>()), y bloquear la memoria de la CPU.

Para obtener una vista más completa de cómo se implementa la API de TensorBuffer, consulta el código fuente de litert_tensor_buffer.h.

Consulta los requisitos de entrada y salida del modelo

Por lo general, el acelerador de hardware especifica los requisitos para asignar un búfer de tensores (TensorBuffer). Los búferes para entradas y salidas pueden tener requisitos en cuanto a la alineación, los pasos de búfer y el tipo de memoria. Puedes usar funciones auxiliares como CreateInputBuffers para controlar automáticamente estos requisitos.

En el siguiente fragmento de código simplificado, se muestra cómo puedes recuperar los requisitos de búfer para los datos de entrada:

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

Para obtener una vista más completa de cómo se implementa la API de TensorBufferRequirements, consulta el código fuente de litert_tensor_buffer_requirements.h.

Crea búferes de tensores administrados (TensorBuffers)

En el siguiente fragmento de código simplificado, se muestra cómo crear búferes de tensores administrados, en los que la API de TensorBuffer asigna los búferes correspondientes:

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

Crea búferes de tensores sin copia

Para unir un búfer existente como un búfer de tensor (sin copia), usa el siguiente fragmento de código:

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

Cómo leer y escribir desde el búfer de tensores

En el siguiente fragmento, se muestra cómo puedes leer de un búfer de entrada y escribir en un búfer de salida:

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

Avanzado: Interoperabilidad de búfer sin copia para tipos de búfer de hardware especializados

Ciertos tipos de búfer, como AHardwareBuffer, permiten la interoperabilidad con otros tipos de búfer. Por ejemplo, se puede crear un búfer de OpenGL a partir de un AHardwareBuffer sin copia. En el siguiente fragmento de código, se muestra un ejemplo:

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

Los búferes de OpenCL también se pueden crear a partir de AHardwareBuffer:

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

En dispositivos móviles que admiten interoperabilidad entre OpenCL y OpenGL, se pueden crear búferes de CL a partir de búferes de 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());

Implementaciones de ejemplo

Consulta las siguientes implementaciones de LiteRT Next en C++.

Inferencia básica (CPU)

La siguiente es una versión condensada de los fragmentos de código de la sección Cómo comenzar. Es la implementación más simple de la inferencia con 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));

Transferencia sin copia con memoria del host

La API de LiteRT Next Compiled Model reduce la fricción de las canalizaciones de inferencia, en especial cuando se trata de varios backends de hardware y flujos de copia cero. En el siguiente fragmento de código, se usa el método CreateFromHostMemory cuando se crea el búfer de entrada, que usa copia cero con la memoria del host.

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