LiteRT Next mit C++ auf Android ausführen

Die LiteRT Next APIs sind in C++ verfügbar und bieten Android-Entwicklern eine größere Kontrolle über die Speicherzuweisung und die Low-Level-Entwicklung als die Kotlin-APIs.

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

Jetzt starten

So fügen Sie Ihrer Android-Anwendung LiteRT Next hinzu:

Buildkonfiguration aktualisieren

Wenn Sie eine C++-Anwendung mit LiteRT für die 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 Beispielkonfiguration kann Ihre Anwendung GPU-, NPU- und CPU-Beschleuniger dynamisch auswählen oder verwenden.

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

  • cc_binary-Regel: Dies ist die grundlegende Bazel-Regel, mit der das C++-Ausführbare Ziel definiert wird (z.B. name = "your_application_name") verwenden.
  • srcs-Attribut:Hier werden die C++-Quelldateien Ihrer Anwendung aufgelistet (z.B. main.cc und andere .cc- oder .h-Dateien).
  • data-Attribut (Laufzeitabhängigkeiten): Dieses Attribut ist entscheidend für das Paketieren freigegebener Bibliotheken und Assets, die Ihre Anwendung zur Laufzeit lädt.
    • LiteRT Core Runtime:Die Haupt-LiteRT-C API-Bibliothek (z.B. //litert/c:litert_runtime_c_api_shared_lib).
    • Dispatch-Bibliotheken:Anbieterspezifische freigegebene Bibliotheken, die LiteRT zur Kommunikation mit den Hardwaretreibern verwendet (z.B. //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • GPU-Backend-Bibliotheken:Die freigegebenen Bibliotheken für die GPU-Beschleunigung (z.B. "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • NPU-Backend-Bibliotheken:Die speziellen freigegebenen 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-Attribut (Kompilierungszeitabhängigkeiten): Hier sind die Bibliotheken aufgeführt, mit denen Ihr Code kompiliert werden muss.
    • LiteRT APIs und ‑Dienstprogramme:Header und statische Bibliotheken für LiteRT-Komponenten wie Tensor-Buffer (z.B. //litert/cc:litert_tensor_buffer).
    • Grafikbibliotheken (für GPU): Abhängigkeiten von Grafik-APIs, die vom GPU-Beschleuniger verwendet werden (z.B. gles_deps()).
  • linkopts-Attribut: Gibt Optionen an, die an den Linker übergeben werden. Dazu kann auch die Verknüpfung 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 durch Erstellen eines Model-Objekts.

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 eine Environment für die CPU- und GPU-Ausführung ohne Optionen erstellt:

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

Compiled Model 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 Ausgabe-Buffer erstellen

Erstellen Sie die erforderlichen Datenstrukturen (Puffer), um die Eingabedaten zu speichern, die Sie dem Modell zur Inferenz zuführen, und die Ausgabedaten, die das Modell nach der Ausführung der Inferenz generiert.

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

Wenn Sie CPU-Speicher 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

Führen Sie das kompilierte Modell mit den in den vorherigen Schritten angegebenen Modell- und Hardwarebeschleunigungsoptionen aus.

compiled_model.Run(input_buffers, output_buffers);

Ausgaben abrufen

Sie können die Ausgabe direkt aus dem Arbeitsspeicher abrufen.

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 Next APIs.

Fehlerbehandlung

In LiteRT wird litert::Expected verwendet, um Werte zurückzugeben oder Fehler ähnlich wie absl::StatusOr oder std::expected zu übertragen. Sie können den Fehler auch manuell prüfen.

Zum besseren Verständnis finden Sie in LiteRT die folgenden Makros:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) weist lhs das Ergebnis von expr zu, wenn kein Fehler auftritt, andernfalls wird der Fehler zurückgegeben.

    Es wird in etwa so aussehen:

    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) führt dieselbe Aktion aus wie LITERT_ASSIGN_OR_RETURN, beendet das Programm aber bei einem Fehler.

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

  • LITERT_ABORT_IF_ERROR(expr) führt dieselbe Aktion wie LITERT_RETURN_IF_ERROR aus, beendet das Programm aber bei einem Fehler.

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, die Anwendung der Hardwarebeschleunigung, die Instanziierung der Laufzeit, das Erstellen von Eingabe- und Ausgabepuffern und die Ausführung der Inferenz verantwortlich.

Das folgende vereinfachte Code-Snippet zeigt, wie die Compiled Model API ein LiteRT-Modell (.tflite) und den Zielhardware-Beschleuniger (GPU) verwendet, um ein kompiliertes Modell zu erstellen, das für die Ausführung von Inferenzen 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 Eingabe- und einen Ausgabebuffer annimmt 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 vollständigere Übersicht zur Implementierung der CompiledModel API finden Sie im Quellcode für litert_compiled_model.h.

Tensor Buffer (TensorBuffer)

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

Eine vollständigere Darstellung der Implementierung der TensorBuffer API finden Sie im Quellcode von litert_tensor_buffer.h.

Anforderungen an die Eingabe-/Ausgabe des Modells abfragen

Die Anforderungen für die Zuweisung eines Tensor-Buffers (TensorBuffer) werden in der Regel vom Hardwarebeschleuniger festgelegt. Für Puffer für Eingaben und Ausgaben können Anforderungen an Ausrichtung, Pufferschritte und Speichertyp gelten. Mithilfe von Hilfsfunktionen wie CreateInputBuffers können Sie diese Anforderungen automatisch erfüllen.

Im folgenden vereinfachten Code-Snippet wird gezeigt, wie Sie die Pufferanforderungen für Eingabedaten abrufen:

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

Eine vollständigere Übersicht zur Implementierung der TensorBufferRequirements API finden Sie im Quellcode von litert_tensor_buffer_requirements.h.

Verwaltete Tensor-Buffer (TensorBuffers) erstellen

Das folgende vereinfachte Code-Snippet zeigt, wie verwaltete Tensor-Buffer erstellt werden, wobei die entsprechenden Buffers von der TensorBuffer API zugewiesen werden:

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-Buffer mit Zero-Copy-Technologie erstellen

Verwenden Sie das folgende Code-Snippet, um einen vorhandenen Puffer als Tensor-Puffer (Zero-Copy) zu verpacken:

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

Lesen und Schreiben aus dem Tensor-Puffer

Im folgenden Snippet wird gezeigt, wie Sie aus einem Eingabepuffer lesen und in einen Ausgabepuffer schreiben:

// 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-Interoperabilität für spezielle Hardwarepuffertypen

Bestimmte Puffertypen wie AHardwareBuffer ermöglichen die Interoperabilität mit anderen Puffertypen. So kann beispielsweise ein OpenGL-Puffer mit Zero-Copy 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-Buffer können auch aus folgenden AHardwareBuffer-Elementen 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-Buffer aus GL-Buffern 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

Weitere Informationen finden Sie in den folgenden Implementierungen von LiteRT Next in C++.

Grundlegende Inferenz (CPU)

Im Folgenden finden Sie eine komprimierte Version der Code-Snippets aus dem Abschnitt Einstieg. Dies ist die einfachste Implementierung der Inferenz mit 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 mit Hostspeicher

Die LiteRT Next Compiled Model API reduziert die Reibung bei Inferenzpipelines, insbesondere bei mehreren Hardware-Backends und Zero-Copy-Flüssen. Im folgenden Code-Snippet wird die CreateFromHostMemory-Methode zum Erstellen des Eingabepuffers verwendet, bei der Zero-Copy mit Hostspeicher verwendet wird.

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