Executar a API de modelo compilado do LiteRT no Android com C++

As APIs de modelo compilado LiteRT estão disponíveis em C++, oferecendo aos desenvolvedores Android controle refinado sobre a alocação de memória e o desenvolvimento de baixo nível.

Para um exemplo de um aplicativo LiteRT em C++, consulte a demonstração de segmentação assíncrona com C++.

Começar

Siga estas etapas para adicionar a API LiteRT Compiled Model ao seu aplicativo Android.

Atualizar a configuração de build

Para criar um aplicativo em C++ com o LiteRT para aceleração de GPU, NPU e CPU usando o Bazel, é necessário definir uma regra cc_binary para garantir que todos os componentes necessários sejam compilados, vinculados e empacotados. O exemplo de configuração a seguir permite que seu aplicativo escolha ou use dinamicamente aceleradores de GPU, NPU e CPU.

Estes são os principais componentes na configuração de build do Bazel:

  • Regra cc_binary:é a regra fundamental do Bazel usada para definir seu destino executável em C++ (por exemplo, name = "your_application_name").
  • Atributo srcs:lista os arquivos de origem C++ do aplicativo (por exemplo, main.cc e outros arquivos .cc ou .h).
  • Atributo data (dependências de tempo de execução): é essencial para empacotar bibliotecas compartilhadas e recursos que o aplicativo carrega durante a execução.
    • Ambiente de execução principal do LiteRT:a principal biblioteca compartilhada da API C do LiteRT (por exemplo, //litert/c:litert_runtime_c_api_shared_lib).
    • Bibliotecas de envio:bibliotecas compartilhadas específicas do fornecedor que o LiteRT usa para se comunicar com os drivers de hardware (por exemplo, //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • Bibliotecas de back-end de GPU:as bibliotecas compartilhadas para aceleração de GPU (por exemplo, "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Bibliotecas de back-end da NPU:as bibliotecas compartilhadas específicas para aceleração de NPU, como as bibliotecas QNN HTP da Qualcomm (por exemplo, @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • Arquivos e recursos do modelo:arquivos do modelo treinado, imagens de teste, shaders ou outros dados necessários durante a execução (por exemplo, :model_files, :shader_files).
  • Atributo deps (dependências de tempo de compilação): lista as bibliotecas que seu código precisa para compilação.
    • APIs e utilitários do LiteRT:cabeçalhos e bibliotecas estáticas para componentes do LiteRT, como buffers de tensor (por exemplo, //litert/cc:litert_tensor_buffer).
    • Bibliotecas de gráficos (para GPU): dependências relacionadas a APIs de gráficos se o acelerador de GPU as usar (por exemplo, gles_deps()).
  • Atributo linkopts:especifica opções transmitidas ao vinculador, que podem incluir a vinculação a bibliotecas do sistema (por exemplo, -landroid para builds do Android ou bibliotecas GLES com gles_linkopts()).

Confira abaixo um exemplo de regra 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
)

Carregar o modelo

Depois de conseguir um modelo do LiteRT ou converter um modelo para o formato .tflite, carregue o modelo criando um objeto Model.

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

Criar o ambiente

O objeto Environment fornece um ambiente de execução que inclui componentes como o caminho do plug-in do compilador e contextos de GPU. O Environment é obrigatório ao criar CompiledModel e TensorBuffer. O código a seguir cria um Environment para execução de CPU e GPU sem opções:

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

Criar o modelo compilado

Usando a API CompiledModel, inicialize o ambiente de execução com o objeto Model recém-criado. É possível especificar a aceleração de hardware neste ponto (kLiteRtHwAcceleratorCpu ou kLiteRtHwAcceleratorGpu):

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

Criar buffers de entrada e saída

Crie as estruturas de dados (buffers) necessárias para armazenar os dados de entrada que você vai inserir no modelo para inferência e os dados de saída que o modelo produz após a execução da inferência.

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

Se você estiver usando a memória da CPU, preencha as entradas gravando dados diretamente no primeiro buffer de entrada.

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

Invocar o modelo

Forneça os buffers de entrada e saída e execute o modelo compilado com o modelo e a aceleração de hardware especificados nas etapas anteriores.

compiled_model.Run(input_buffers, output_buffers);

Recuperar saídas

Recuperar saídas lendo diretamente a saída do modelo da memória.

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

Principais conceitos e componentes

Consulte as seções a seguir para informações sobre os principais conceitos e componentes das APIs de modelo compilado do LiteRT.

Tratamento de erros

O LiteRT usa litert::Expected para retornar valores ou propagar erros de maneira semelhante a absl::StatusOr ou std::expected. Você pode verificar manualmente se há erros.

Para sua conveniência, o LiteRT fornece as seguintes macros:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) atribui o resultado de expr a lhs se não produzir um erro. Caso contrário, retorna o erro.

    Ele vai ser expandido para algo como o snippet a seguir.

    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) faz o mesmo que LITERT_ASSIGN_OR_RETURN, mas encerra o programa em caso de erro.

  • LITERT_RETURN_IF_ERROR(expr) retorna expr se a avaliação dele produzir um erro.

  • LITERT_ABORT_IF_ERROR(expr) faz o mesmo que LITERT_RETURN_IF_ERROR, mas interrompe o programa em caso de erro.

Para mais informações sobre macros LiteRT, consulte litert_macros.h.

Modelo compilado (CompiledModel)

A API Compiled Model (CompiledModel) é responsável por carregar um modelo, aplicar aceleração de hardware, instanciar o tempo de execução, criar buffers de entrada e saída e executar a inferência.

O snippet de código simplificado a seguir demonstra como a API Compiled Model usa um modelo LiteRT (.tflite) e o acelerador de hardware de destino (GPU) para criar um modelo compilado pronto para executar a inferência.

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

O snippet de código simplificado a seguir demonstra como a API Compiled Model recebe um buffer de entrada e saída e executa inferências com o 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 uma visão mais completa de como a API CompiledModel é implementada, consulte o código-fonte de litert_compiled_model.h.

Buffer de tensor (TensorBuffer)

O LiteRT oferece suporte integrado à interoperabilidade de buffer de E/S usando a API Tensor Buffer (TensorBuffer) para processar o fluxo de dados de entrada e saída do modelo compilado. A API Tensor Buffer permite gravar (Write<T>()) e ler (Read<T>()), além de bloquear a memória da CPU.

Para uma visão mais completa de como a API TensorBuffer é implementada, consulte o código-fonte de litert_tensor_buffer.h.

Requisitos de entrada/saída do modelo de consulta

Os requisitos para alocar um Tensor Buffer (TensorBuffer) geralmente são especificados pelo acelerador de hardware. Os buffers para entradas e saídas podem ter requisitos relacionados a alinhamento, strides de buffer e tipo de memória. É possível usar funções auxiliares, como CreateInputBuffers, para processar automaticamente esses requisitos.

O snippet de código simplificado a seguir demonstra como recuperar os requisitos de buffer para dados de entrada:

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

Para uma visão mais completa de como a API TensorBufferRequirements é implementada, consulte o código-fonte de litert_tensor_buffer_requirements.h.

Criar buffers de tensor gerenciados (TensorBuffers)

O snippet de código simplificado a seguir demonstra como criar Managed Tensor Buffers, em que a API TensorBuffer aloca os buffers 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));

Criar buffers de tensor com cópia zero

Para encapsular um buffer existente como um TensorBuffer (cópia zero), use o seguinte snippet 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));

Como ler e gravar no buffer de tensor

O snippet a seguir demonstra como ler de um buffer de entrada e gravar em um buffer de saída:

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

Avançado: interoperabilidade de buffer de cópia zero para tipos de buffer de hardware especializados

Alguns tipos de buffer, como AHardwareBuffer, permitem a interoperabilidade com outros tipos de buffer. Por exemplo, um buffer do OpenGL pode ser criado de um AHardwareBuffer com cópia zero. O snippet de código a seguir mostra um exemplo:

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

Os buffers OpenCL também podem ser criados com AHardwareBuffer:

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

Em dispositivos móveis que oferecem suporte à interoperabilidade entre OpenCL e OpenGL, os buffers CL podem ser criados com base em buffers 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());

Exemplos de implementações

Consulte as seguintes implementações do LiteRT em C++.

Inferência básica (CPU)

Confira a seguir uma versão condensada dos snippets de código da seção Começar. É a implementação mais simples da inferência com o 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));

Cópia zero com memória do host

A API LiteRT Compiled Model reduz o atrito dos pipelines de inferência, principalmente ao lidar com vários backends de hardware e fluxos de cópia zero. O snippet de código a seguir usa o método CreateFromHostMemory ao criar o buffer de entrada, que usa cópia zero com memória do 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);