LiteRT Compiled Model API unter Android mit C++ ausführen

Die LiteRT Compiled Model APIs sind in C++ verfügbar und bieten Android-Entwicklern eine detaillierte Kontrolle über die Speicherzuweisung und die Entwicklung auf niedriger Ebene.

Ein Beispiel für eine LiteRT-Anwendung in C++ finden Sie in der Demo zur asynchronen Segmentierung mit C++.

Jetzt starten

Führen Sie die folgenden Schritte aus, um die LiteRT Compiled Model API zu Ihrer Android-Anwendung hinzuzufügen.

Build-Konfiguration aktualisieren

Wenn Sie eine C++-Anwendung mit LiteRT für GPU-, NPU- und CPU-Beschleunigung mit Bazel erstellen, müssen Sie eine cc_binary-Regel definieren, damit alle erforderlichen Komponenten kompiliert, verknüpft und verpackt werden. Mit der folgenden Einrichtung kann Ihre Anwendung GPU-, NPU- und CPU-Beschleuniger dynamisch auswählen oder nutzen.

Hier sind die wichtigsten Komponenten in Ihrer Bazel-Build-Konfiguration:

  • cc_binary-Regel:Dies ist die grundlegende Bazel-Regel, mit der das ausführbare C++-Ziel definiert wird (z.B. name = "your_application_name") verwenden.
  • srcs-Attribut:Hier werden die C++-Quelldateien Ihrer Anwendung aufgeführt, z.B. main.cc und andere .cc- oder .h-Dateien).
  • data Attribute (Laufzeitabhängigkeiten): Dies ist entscheidend für das Verpacken von gemeinsam genutzten Bibliotheken und Assets, die Ihre Anwendung zur Laufzeit lädt.
    • LiteRT Core Runtime:Die primäre gemeinsam genutzte LiteRT C-API-Bibliothek (z.B. //litert/c:litert_runtime_c_api_shared_lib).
    • Dispatch-Bibliotheken:Anbieterspezifische freigegebene Bibliotheken, die LiteRT für die Kommunikation mit den Hardwaretreibern verwendet (z.B. //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • GPU-Backend-Bibliotheken:Die gemeinsam genutzten Bibliotheken für die GPU-Beschleunigung (z.B. "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • NPU-Backend-Bibliotheken:Die spezifischen gemeinsam genutzten Bibliotheken für die NPU-Beschleunigung, z. B. die QNN HTP-Bibliotheken von Qualcomm (z. B. @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • Modelldateien und Assets:Ihre trainierten Modelldateien, Testbilder, Shader oder andere Daten, die zur Laufzeit benötigt werden (z.B. :model_files, :shader_files).
  • deps-Attribute (Kompilierungszeitabhängigkeiten): Hier werden die Bibliotheken aufgeführt, für die Ihr Code kompiliert werden muss.
    • LiteRT-APIs und ‑Dienstprogramme:Header und statische Bibliotheken für LiteRT-Komponenten wie Tensor-Puffer (z.B. //litert/cc:litert_tensor_buffer).
    • Grafikbibliotheken (für GPU): Abhängigkeiten im Zusammenhang mit Grafik-APIs, wenn der GPU-Beschleuniger sie verwendet (z.B. gles_deps()).
  • linkopts-Attribute:Gibt Optionen an, die an den Linker übergeben werden. Dazu kann auch das Verknüpfen mit Systembibliotheken gehören (z. B. -landroid für Android-Builds oder GLES-Bibliotheken mit gles_linkopts()).

Das folgende Beispiel zeigt eine cc_binary-Regel:

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
)

Modell laden

Nachdem Sie ein LiteRT-Modell erhalten oder ein Modell in das .tflite-Format konvertiert haben, laden Sie das Modell, indem Sie ein Model-Objekt erstellen.

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

Umgebung erstellen

Das Environment-Objekt bietet eine Laufzeitumgebung, die Komponenten wie den Pfad des Compiler-Plug-ins und GPU-Kontexte enthält. Die Environment ist erforderlich, wenn Sie CompiledModel und TensorBuffer erstellen. Mit dem folgenden Code wird ein Environment für die CPU- und GPU-Ausführung ohne Optionen erstellt:

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

Kompiliertes Modell erstellen

Initialisieren Sie die Laufzeit mit dem neu erstellten Model-Objekt über die CompiledModel API. Sie können die Hardwarebeschleunigung an dieser Stelle angeben (kLiteRtHwAcceleratorCpu oder kLiteRtHwAcceleratorGpu):

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

Eingabe- und Ausgabepuffer erstellen

Erstellen Sie die erforderlichen Datenstrukturen (Puffer) für die Eingabedaten, die Sie in das Modell für die Inferenz einfügen, und für die Ausgabedaten, die das Modell nach der Inferenz erzeugt.

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

Wenn Sie den CPU-Arbeitsspeicher verwenden, füllen Sie die Eingaben, indem Sie Daten direkt in den ersten Eingabepuffer schreiben.

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

Modell aufrufen

Stellen Sie die Ein- und Ausgabepuffer bereit und führen Sie das kompilierte Modell mit dem Modell und der Hardwarebeschleunigung aus, die in den vorherigen Schritten angegeben wurden.

compiled_model.Run(input_buffers, output_buffers);

Ausgaben abrufen

Ausgaben abrufen, indem die Modellausgabe direkt aus dem Speicher gelesen wird.

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

Wichtige Konzepte und Komponenten

In den folgenden Abschnitten finden Sie Informationen zu den wichtigsten Konzepten und Komponenten der LiteRT Compiled Model APIs.

Fehlerbehandlung

LiteRT verwendet litert::Expected, um entweder Werte zurückzugeben oder Fehler auf ähnliche Weise wie absl::StatusOr oder std::expected weiterzugeben. Sie können den Fehler auch manuell prüfen.

LiteRT bietet die folgenden Makros:

  • Mit LITERT_ASSIGN_OR_RETURN(lhs, expr) wird das Ergebnis von expr lhs zugewiesen, wenn kein Fehler auftritt. Andernfalls wird der Fehler zurückgegeben.

    Es wird in etwa wie im folgenden Snippet erweitert.

    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) macht dasselbe wie LITERT_ASSIGN_OR_RETURN, bricht das Programm aber im Fehlerfall ab.

  • LITERT_RETURN_IF_ERROR(expr) gibt expr zurück, wenn bei der Auswertung ein Fehler auftritt.

  • LITERT_ABORT_IF_ERROR(expr) hat dieselbe Funktion wie LITERT_RETURN_IF_ERROR, bricht das Programm aber im Fehlerfall ab.

Weitere Informationen zu LiteRT-Makros finden Sie unter litert_macros.h.

Kompiliertes Modell (CompiledModel)

Die Compiled Model API (CompiledModel) ist für das Laden eines Modells, das Anwenden von Hardwarebeschleunigung, das Instanziieren der Laufzeit, das Erstellen von Ein- und Ausgabepuffern und das Ausführen der Inferenz verantwortlich.

Das folgende vereinfachte Code-Snippet zeigt, wie die Compiled Model API ein LiteRT-Modell (.tflite) und den Zielhardwarebeschleuniger (GPU) verwendet und ein kompiliertes Modell erstellt, das für die Ausführung von Inferenz bereit ist.

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

Das folgende vereinfachte Code-Snippet zeigt, wie die Compiled Model API einen Ein- und Ausgabepuffer verwendet und Inferenzen mit dem kompilierten Modell ausführt.

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

Eine umfassendere Ansicht der Implementierung der CompiledModel API finden Sie im Quellcode für litert_compiled_model.h.

Tensor-Puffer (TensorBuffer)

LiteRT bietet integrierte Unterstützung für die Interoperabilität von E/A-Puffern. Dazu wird die Tensor Buffer API (TensorBuffer) verwendet, um den Datenfluss in das und aus dem kompilierten Modell zu verarbeiten. Mit der Tensor Buffer API können Sie CPU-Speicher schreiben (Write<T>()), lesen (Read<T>()) und sperren.

Eine umfassendere Ansicht der Implementierung der TensorBuffer API finden Sie im Quellcode für litert_tensor_buffer.h.

Anforderungen an die Eingabe/Ausgabe von Abfragemodellen

Die Anforderungen für die Zuweisung eines Tensor-Puffers (TensorBuffer) werden in der Regel vom Hardwarebeschleuniger angegeben. Für Puffer für Ein- und Ausgaben können Anforderungen hinsichtlich Ausrichtung, Puffer-Strides und Speichertyp gelten. Mit Hilfsfunktionen wie CreateInputBuffers können Sie diese Anforderungen automatisch erfüllen.

Das folgende vereinfachte Code-Snippet zeigt, wie Sie die Pufferanforderungen für Eingabedaten abrufen können:

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

Eine umfassendere Ansicht der Implementierung der TensorBufferRequirements API finden Sie im Quellcode für litert_tensor_buffer_requirements.h.

Verwaltete Tensor-Puffer (TensorBuffers) erstellen

Das folgende vereinfachte Code-Snippet zeigt, wie verwaltete Tensor-Puffer erstellt werden, wobei die TensorBuffer API die entsprechenden Puffer zuweist:

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

Tensor-Puffer mit Zero-Copy erstellen

Wenn Sie einen vorhandenen Puffer als Tensor-Puffer (Zero-Copy) umschließen möchten, verwenden Sie das folgende Code-Snippet:

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

Aus Tensor-Puffer lesen und in Tensor-Puffer schreiben

Das folgende Snippet zeigt, wie Sie aus einem Eingabepuffer lesen und in einen Ausgabepuffer schreiben können:

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

Erweitert: Zero-Copy-Puffer-Interop für spezielle Hardwarepuffertypen

Bestimmte Puffertypen wie AHardwareBuffer ermöglichen die Interoperabilität mit anderen Puffertypen. Beispielsweise kann ein OpenGL-Puffer ohne Kopieren aus einem AHardwareBuffer erstellt werden. Das folgende Code-Snippet zeigt ein Beispiel:

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

OpenCL-Puffer können auch aus AHardwareBuffer erstellt werden:

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

Auf Mobilgeräten, die die Interoperabilität zwischen OpenCL und OpenGL unterstützen, können CL-Puffer aus GL-Puffern erstellt werden:

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

Beispielimplementierungen

Hier finden Sie Implementierungen von LiteRT in C++.

Einfache Inferenz (CPU)

Im Folgenden finden Sie eine gekürzte Version der Code-Snippets aus dem Abschnitt Erste Schritte. Dies ist die einfachste Implementierung der Inferenz mit 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));

Zero-Copy mit Hostspeicher

Die LiteRT Compiled Model API reduziert die Reibung von Inferenzpipelines, insbesondere bei der Verarbeitung mehrerer Hardware-Back-Ends und Zero-Copy-Abläufe. Im folgenden Code-Snippet wird die Methode CreateFromHostMemory beim Erstellen des Eingabepuffers verwendet, der Zero-Copy mit dem Hostspeicher nutzt.

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