API de C++ de LiteRT CompiledModel

La API de LiteRT CompiledModel está disponible en C++, lo que les brinda a los desarrolladores un control detallado sobre la asignación de memoria y el desarrollo de bajo nivel. Para ver un ejemplo, consulta la app de segmentación de imágenes en C++.

En la siguiente guía, se muestra la inferencia básica de la CPU de la API de CompiledModel de Kotlin. Consulta la guía sobre la aceleración por GPU y la aceleración por NPU para conocer las funciones de aceleración avanzadas.

Cómo agregar una dependencia de compilación

Elige la ruta que se adapte a tu proyecto:

  • Usar biblioteca compilada previamente (Android): Usa la biblioteca compilada previamente del paquete de Maven de LiteRT para realizar la configuración de inmediato. Consulta cómo usar la biblioteca de C++ compilada previamente desde LiteRT Maven.

  • Compilación desde la fuente (plataformas múltiples): Compila desde la fuente para tener control total y compatibilidad con múltiples plataformas (Android, iOS, macOS, Linux y Windows). Consulta las siguientes instrucciones.

Compila desde la fuente con Bazel

Compilar una aplicación en 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 utilice 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 destino ejecutable de C++ (p.ej., name = "your_application_name").
  • Atributo srcs: Enumera los archivos fuente de C++ de tu aplicación (p.ej., main.cc y otros archivos .cc o .h).
  • Atributo data (dependencias de tiempo de ejecución): Es fundamental para empaquetar bibliotecas compartidas y recursos que tu aplicación carga en el tiempo de ejecución.
    • Tiempo de ejecución principal de LiteRT: Es la biblioteca compartida de la API de LiteRT en C principal (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 por GPU (p.ej., "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Bibliotecas de backend de la NPU: Son las bibliotecas compartidas específicas para la aceleración de la NPU, como las bibliotecas 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 del 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 en tiempo de compilación): En esta sección, se enumeran las bibliotecas con las que tu código debe compilarse.
    • LiteRT y utilidades: 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): Dependencias relacionadas con la API 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()).

A continuación, se muestra un ejemplo de una regla de 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
)

Inferencia básica

Carga el modelo

Después de obtener un modelo de LiteRT o convertir un modelo 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. El campo 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 CompiledModel

Con la API de CompiledModel, inicializa el entorno de ejecución con el objeto Model recién creado. En este punto, puedes especificar la aceleración de hardware (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 (búferes) necesarias para contener los datos de entrada que ingresarás en el 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 memoria de CPU, completa las entradas escribiendo datos directamente en el primer búfer de entrada.

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

Invoca el modelo

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

compiled_model.Run(input_buffers, output_buffers);

Recupera resultados

Recupera los resultados leyendo directamente el resultado del modelo de 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 la API de LiteRT CompiledModel.

Manejo de errores

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

Para mayor comodidad, LiteRT proporciona las siguientes macros:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) asigna el resultado de expr a lhs si no produce un error y, de lo contrario, devuelve 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 anula el programa en caso de error.

  • LITERT_RETURN_IF_ERROR(expr) devuelve expr si su evaluación produce 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.

Búfer de tensor (TensorBuffer)

LiteRT proporciona compatibilidad integrada para la interoperabilidad del búfer de E/S, ya que usa la API de Tensor Buffer (TensorBuffer) para controlar el flujo de datos hacia el modelo compilado y desde él. La API de Tensor Buffer permite 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.

Requisitos de entrada y salida del modelo de consulta

Los requisitos para asignar un búfer de tensor (TensorBuffer) suelen especificarse en el acelerador de hardware. Los búferes de entradas y salidas pueden tener requisitos relacionados con la alineación, los avances de búfer y el tipo de memoria. Puedes usar funciones de ayuda, 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 respectivos:

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 con copiado nulo

Para encapsular un búfer existente como un Tensor Buffer (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));

Lectura y escritura desde Tensor Buffer

En el siguiente fragmento, se muestra cómo puedes leer desde 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 de copia cero 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 con copia cero. 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());

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

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

En dispositivos móviles que admiten la 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 en C++.

Inferencia básica (CPU)

A continuación, se muestra una versión condensada de los fragmentos de código de la sección Primeros pasos. Es la implementación más simple de la inferencia con 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));

Copia cero con memoria del host

La API de LiteRT CompiledModel reduce la fricción de las canalizaciones de inferencia, en especial cuando se trabaja con 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 la 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);