Esegui LiteRT Next su Android con C++

Le API LiteRT Next sono disponibili in C++ e possono offrire agli sviluppatori Android un maggiore controllo sull'allocazione della memoria e sullo sviluppo a basso livello rispetto alle API Kotlin.

Per un esempio di applicazione LiteRT Next in C++, consulta la demo di segmentazione asincrona con C++.

Inizia

Per aggiungere LiteRT Next alla tua applicazione Android, segui questa procedura.

Aggiorna la configurazione di compilazione

La creazione di un'applicazione C++ con LiteRT per l'accelerazione GPU, NPU e CPU utilizzando Bazel prevede la definizione di una regola cc_binary per garantire che tutti gli elementi necessari vengano compilati, collegati e pacchettizzati. La configurazione di seguito consente all'applicazione di scegliere o utilizzare dinamicamente gli acceleratori GPU, NPU e CPU.

Ecco i componenti chiave della configurazione di compilazione di Bazel:

  • cc_binary Regola:questa è la regola di Bazel di base utilizzata per definire il target eseguibile C++ (ad es. name = "your_application_name").
  • Attributo srcs: elenca i file di origine C++ dell'applicazione (ad es. main.cc e altri file .cc o .h).
  • Attributo data (dipendenze di runtime): è fondamentale per il packaging delle librerie e degli asset condivisi che l'applicazione carica in fase di runtime.
    • LiteRT Core Runtime:la libreria condivisa dell'API C LiteRT principale (ad es. //litert/c:litert_runtime_c_api_shared_lib).
    • Librerie di inoltro:librerie condivise specifiche del fornitore che LiteRT utilizza per comunicare con i driver hardware (ad es. //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • Librerie di backend GPU:le librerie condivise per l'accelerazione GPU (ad es. "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Librerie di backend NPU:le librerie condivise specifiche per l'accelerazione NPU, ad esempio le librerie HTP QNN di Qualcomm (ad es. @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • File e asset del modello:i file del modello addestrato, le immagini di test, gli shader o qualsiasi altro dato necessario in fase di esecuzione (ad es. :model_files, :shader_files).
  • Attributo deps (dipendenze di compilazione): vengono elencate le librerie con cui il codice deve essere compilato.
    • API e utilità LiteRT: intestazioni e librerie statiche per i componenti LiteRT come i buffer di tensori (ad es. //litert/cc:litert_tensor_buffer).
    • Librerie grafiche (per GPU): dipendenze relative alle API di grafica se l'acceleratore GPU le utilizza (ad es. gles_deps()).
  • Attributo linkopts: specifica le opzioni passate al linker, che possono includere il collegamento alle librerie di sistema (ad es. -landroid per le compilazioni Android o librerie GLES con gles_linkopts()).

Di seguito è riportato un esempio di regola 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
)

Carica il modello

Dopo aver ottenuto un modello LiteRT o aver convertito un modello nel formato .tflite, caricalo creando un oggetto Model.

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

Crea l'ambiente

L'oggetto Environment fornisce un ambiente di runtime che include componenti come il percorso del plug-in del compilatore e i contesti GPU. Environment è obbligatorio quando crei CompiledModel e TensorBuffer. Il seguente codice crea un Environment per l'esecuzione su CPU e GPU senza opzioni:

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

Crea il modello compilato

Utilizza l'API CompiledModel per inizializzare il runtime con l'oggetto Model appena creato. A questo punto puoi specificare l'accelerazione hardware (kLiteRtHwAcceleratorCpu o kLiteRtHwAcceleratorGpu):

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

Creare buffer di input e output

Crea le strutture di dati (buffer) necessarie per contenere i dati di input che alimenterai nel modello per l'inferenza e i dati di output prodotti dal modello dopo l'esecuzione dell'inferenza.

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

Se utilizzi la memoria della CPU, compila gli input scrivendo i dati direttamente nel primo buffer di input.

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

Richiama il modello

Fornendo i buffer di input e output, esegui il modello compilato con il modello e l'accelerazione hardware specificati nei passaggi precedenti.

compiled_model.Run(input_buffers, output_buffers);

Recupera gli output

Recupera gli output leggendo direttamente l'output del modello dalla memoria.

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

Concetti e componenti chiave

Fai riferimento alle sezioni seguenti per informazioni sui concetti e sui componenti chiave delle API LiteRT Next.

Gestione degli errori

LiteRT utilizza litert::Expected per restituire valori o propagare errori in modo simile a absl::StatusOr o std::expected. Puoi controllare manualmente la presenza dell'errore.

Per comodità, LiteRT fornisce le seguenti macro:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) assegna il risultato di expr a lhs se non genera un errore e altrimenti restituisce l'errore.

    Verrà visualizzato uno snippet simile al seguente.

    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) ha lo stesso effetto di LITERT_ASSIGN_OR_RETURN, ma interrompe il programma in caso di errore.

  • LITERT_RETURN_IF_ERROR(expr) restituisce expr se la relativa valutazione produce un errore.

  • LITERT_ABORT_IF_ERROR(expr) fa lo stesso di LITERT_RETURN_IF_ERROR, ma interrompe il programma in caso di errore.

Per ulteriori informazioni sulle macro LiteRT, consulta litert_macros.h.

Modello compilato (CompiledModel)

L'API Modello compilato (CompiledModel) è responsabile del caricamento di un modello, dell'applicazione dell'accelerazione hardware, dell'inizializzazione del runtime, della creazione di buffer di input e di output e dell'esecuzione dell'inferenza.

Il seguente snippet di codice semplificato mostra come l'API Modello compilato prenda un modello LiteRT (.tflite) e l'acceleratore hardware di destinazione (GPU) e crei un modello compilato pronto per l'esecuzione dell'inferenza.

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

Il seguente snippet di codice semplificato mostra come l'API Modello compilato prenda un buffer di input e di output ed esegua deduzioni con il modello compilato.

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

Per una visione più completa dell'implementazione dell'API CompiledModel, consulta il codice sorgente di litert_compiled_model.h.

Buffer di tensori (TensorBuffer)

LiteRT Next fornisce il supporto integrato per l'interoperabilità dei buffer I/O, utilizzando l'API Tensor Buffer (TensorBuffer) per gestire il flusso di dati all'interno e all'esterno del modello compilato. L'API Tensor Buffer offre la possibilità di scrivere (Write<T>()) e leggere (Read<T>()) e di bloccare la memoria della CPU.

Per una visione più completa dell'implementazione dell'API TensorBuffer, consulta il codice sorgente di litert_tensor_buffer.h.

Requisiti di input/output del modello di query

I requisiti per l'allocazione di un tensore buffer (TensorBuffer) sono in genere specificati dall'acceleratore hardware. I buffer per input e output possono avere requisiti relativi ad allineamento, passi del buffer e tipo di memoria. Puoi utilizzare funzioni di supporto come CreateInputBuffers per gestire automaticamente questi requisiti.

Il seguente snippet di codice semplificato mostra come recuperare i requisiti del buffer per i dati di input:

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

Per una visione più completa dell'implementazione dell'API TensorBufferRequirements, consulta il codice sorgente di litert_tensor_buffer_requirements.h.

Crea buffer di tensori gestiti (TensorBuffers)

Il seguente snippet di codice semplificato mostra come creare Managed Tensor Buffers, in cui l'API TensorBuffer alloca i rispettivi buffer:

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

Creare buffer di tensori con zero-copy

Per avvolgere un buffer esistente come tensore (senza copia), utilizza lo snippet di codice seguente:

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

Lettura e scrittura da Tensor Buffer

Il seguente snippet mostra come leggere da un buffer di input e scrivere in un buffer di output:

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

Avanzato: interoperabilità dei buffer a copia zero per tipi di buffer hardware specializzati

Alcuni tipi di buffer, come AHardwareBuffer, consentono l'interoperabilità con altri tipi di buffer. Ad esempio, è possibile creare un buffer OpenGL da un AHardwareBuffer con zero-copy. Il seguente snippet di codice mostra un esempio:

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

È anche possibile creare buffer OpenCL da AHardwareBuffer:

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

Sui dispositivi mobili che supportano l'interoperabilità tra OpenCL e OpenGL, è possibile creare buffer CL dai buffer 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());

Esempi di implementazioni

Fai riferimento alle seguenti implementazioni di LiteRT Next in C++.

Interruzione di base (CPU)

Di seguito è riportata una versione condensata degli snippet di codice della sezione Inizia. È l'implementazione più semplice dell'inferenza 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));

Zero-copy con la memoria dell'host

L'API LiteRT Next Compiled Model riduce le difficoltà delle pipeline di inferenza, soprattutto quando si ha a che fare con più backend hardware e flussi di zero-copy. Il seguente snippet di codice utilizza il metodo CreateFromHostMemory per creare il buffer di input, che utilizza la copia zero con la memoria dell'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);