Uruchamianie interfejsu API skompilowanego modelu LiteRT na Androidzie za pomocą C++

Interfejsy API skompilowanego modelu LiteRT są dostępne w C++, co daje deweloperom na Androida szczegółową kontrolę nad przydzielaniem pamięci i programowaniem niskiego poziomu.

Przykładową aplikację LiteRT w C++ znajdziesz w demonstracji asynchronicznej segmentacji w C++.

Rozpocznij

Aby dodać interfejs LiteRT Compiled Model API do aplikacji na Androida, wykonaj te czynności.

Aktualizowanie konfiguracji kompilacji

Tworzenie aplikacji w C++ z LiteRT na potrzeby akceleracji GPU, NPU i CPU za pomocą Bazela wymaga zdefiniowania reguły cc_binary, aby zapewnić skompilowanie, połączenie i spakowanie wszystkich niezbędnych komponentów. Poniższa konfiguracja umożliwia aplikacji dynamiczne wybieranie lub wykorzystywanie akceleratorów GPU, NPU i CPU.

Oto kluczowe komponenty konfiguracji kompilacji Bazel:

  • cc_binary Reguła: to podstawowa reguła Bazela używana do definiowania docelowego pliku wykonywalnego w C++ (np. name = "your_application_name").
  • srcs Atrybut: zawiera listę plików źródłowych C++ aplikacji (np. main.cc i inne pliki .cc lub .h).
  • data Atrybut (zależności w czasie działania): jest on kluczowy w przypadku pakowania bibliotek i zasobów współdzielonych, które aplikacja wczytuje w czasie działania.
    • LiteRT Core Runtime: główna biblioteka współdzielona interfejsu LiteRT C API (np. //litert/c:litert_runtime_c_api_shared_lib).
    • Biblioteki wysyłania: biblioteki współdzielone specyficzne dla dostawcy, których LiteRT używa do komunikacji ze sterownikami sprzętu (np. //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • Biblioteki backendu GPU: biblioteki wspólne do akceleracji GPU (np. "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Biblioteki backendu NPU: konkretne biblioteki udostępnione do akceleracji NPU, np.biblioteki QNN HTP firmy Qualcomm (np. @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • Pliki i zasoby modelu: wytrenowane pliki modelu, obrazy testowe, shadery lub inne dane potrzebne w czasie działania (np. :model_files, :shader_files).
  • deps Atrybut (zależności w czasie kompilacji): zawiera listę bibliotek, z którymi musi być skompilowany Twój kod.
    • Interfejsy API i narzędzia LiteRT: pliki nagłówkowe i biblioteki statyczne dla komponentów LiteRT, takich jak bufory tensorów (np. //litert/cc:litert_tensor_buffer).
    • Biblioteki graficzne (dla GPU): zależności związane z interfejsami API grafiki, jeśli akcelerator GPU ich używa (np. gles_deps()).
  • linkopts Atrybut: określa opcje przekazywane do linkera, które mogą obejmować linkowanie z bibliotekami systemowymi (np. -landroid w przypadku kompilacji na Androida lub biblioteki GLES z gles_linkopts()).

Oto przykład reguły 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
)

Wczytywanie modelu

Po uzyskaniu modelu LiteRT lub przekonwertowaniu modelu do formatu .tflite załaduj go, tworząc obiekt Model.

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

Tworzenie środowiska

Obiekt Environment zapewnia środowisko wykonawcze, które zawiera komponenty takie jak ścieżka wtyczki kompilatora i konteksty GPU. Właściwość Environment jest wymagana podczas tworzenia właściwości CompiledModelTensorBuffer. Poniższy kod tworzy Environment do wykonywania na procesorze i GPU bez żadnych opcji:

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

Tworzenie skompilowanego modelu

Za pomocą interfejsu CompiledModel API zainicjuj środowisko wykonawcze za pomocą nowo utworzonego obiektu Model. W tym momencie możesz określić akcelerację sprzętową (kLiteRtHwAcceleratorCpu lub kLiteRtHwAcceleratorGpu):

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

Tworzenie buforów wejściowych i wyjściowych

Utwórz niezbędne struktury danych (bufory) do przechowywania danych wejściowych, które będziesz przekazywać do modelu na potrzeby wnioskowania, oraz danych wyjściowych, które model generuje po uruchomieniu wnioskowania.

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

Jeśli używasz pamięci procesora, wypełnij pola wejściowe, wpisując dane bezpośrednio do pierwszego bufora wejściowego.

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

Wywoływanie modelu

Podaj bufory wejściowe i wyjściowe, a następnie uruchom skompilowany model z modelem i akceleracją sprzętową określonymi w poprzednich krokach.

compiled_model.Run(input_buffers, output_buffers);

Pobieranie danych wyjściowych

Pobieraj dane wyjściowe, odczytując je bezpośrednio z pamięci.

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

Kluczowe pojęcia i komponenty

W kolejnych sekcjach znajdziesz informacje o kluczowych koncepcjach i komponentach interfejsów API skompilowanego modelu LiteRT.

Obsługa błędów

LiteRT używa litert::Expected do zwracania wartości lub propagowania błędów w sposób podobny do absl::StatusOr lub std::expected. Możesz samodzielnie sprawdzić, czy występuje błąd.

Aby ułatwić Ci pracę, LiteRT udostępnia te makra:

  • Funkcja LITERT_ASSIGN_OR_RETURN(lhs, expr) przypisuje wynik funkcji expr do funkcji lhs, jeśli nie generuje ona błędu, a w przeciwnym razie zwraca błąd.

    Zostanie rozwinięty do postaci podobnej do poniższego fragmentu kodu.

    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) działa tak samo jak LITERT_ASSIGN_OR_RETURN, ale w przypadku błędu przerywa działanie programu.

  • LITERT_RETURN_IF_ERROR(expr) zwraca expr, jeśli ocena spowoduje błąd.

  • LITERT_ABORT_IF_ERROR(expr) działa tak samo jak LITERT_RETURN_IF_ERROR, ale w przypadku błędu przerywa działanie programu.

Więcej informacji o makrach LiteRT znajdziesz w litert_macros.h.

Skompilowany model (CompiledModel)

Interfejs Compiled Model API (CompiledModel) odpowiada za wczytywanie modelu, stosowanie akceleracji sprzętowej, tworzenie instancji środowiska wykonawczego, tworzenie buforów wejściowych i wyjściowych oraz uruchamianie wnioskowania.

Poniższy uproszczony fragment kodu pokazuje, jak interfejs Compiled Model API przyjmuje model LiteRT (.tflite) i docelowy akcelerator sprzętowy (GPU) oraz tworzy skompilowany model gotowy do uruchomienia wnioskowania.

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

Poniższy uproszczony fragment kodu pokazuje, jak interfejs Compiled Model API przyjmuje bufor wejściowy i wyjściowy oraz uruchamia wnioskowanie za pomocą skompilowanego modelu.

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

Aby uzyskać pełniejszy obraz implementacji interfejsu CompiledModel API, zapoznaj się z kodem źródłowym pliku litert_compiled_model.h.

Bufor tensora (TensorBuffer)

LiteRT zapewnia wbudowaną obsługę interoperacyjności buforów wejścia/wyjścia za pomocą interfejsu Tensor Buffer API (TensorBuffer), który obsługuje przepływ danych do i z skompilowanego modelu. Interfejs Tensor Buffer API umożliwia zapisywanie (Write<T>()) i odczytywanie (Read<T>()) danych oraz blokowanie pamięci procesora.

Aby uzyskać pełniejszy obraz implementacji interfejsu TensorBuffer API, zapoznaj się z kodem źródłowym pliku litert_tensor_buffer.h.

Wymagania dotyczące danych wejściowych i wyjściowych modelu zapytania

Wymagania dotyczące przydzielania bufora tensora (TensorBuffer) są zwykle określane przez akcelerator sprzętowy. Bufory danych wejściowych i wyjściowych mogą mieć wymagania dotyczące wyrównania, kroków bufora i typu pamięci. Możesz użyć funkcji pomocniczych, takich jak CreateInputBuffers, aby automatycznie spełniać te wymagania.

Poniższy uproszczony fragment kodu pokazuje, jak pobrać wymagania dotyczące bufora danych wejściowych:

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

Aby uzyskać pełniejszy obraz sposobu implementacji interfejsu TensorBufferRequirements API, zapoznaj się z kodem źródłowym pliku litert_tensor_buffer_requirements.h.

Tworzenie zarządzanych buforów tensorów (TensorBuffers)

Poniższy uproszczony fragment kodu pokazuje, jak utworzyć bufor Managed TensorBuffer, w którym interfejs TensorBuffer API przydziela odpowiednie bufory:

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

Tworzenie buforów tensorowych bez kopiowania

Aby opakować istniejący bufor jako bufor tensora (bez kopiowania), użyj tego fragmentu kodu:

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

Odczytywanie i zapisywanie w buforze tensora

Poniższy fragment kodu pokazuje, jak odczytywać dane z bufora wejściowego i zapisywać je w buforze wyjściowym:

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

Zaawansowane: interoperacyjność bufora bez kopiowania w przypadku specjalistycznych typów buforów sprzętowych

Niektóre typy buforów, np. AHardwareBuffer, umożliwiają współdziałanie z innymi typami buforów. Na przykład bufor OpenGL można utworzyć z obiektu AHardwareBuffer bez kopiowania. Poniższy fragment kodu pokazuje przykład:

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

Bufory OpenCL można też tworzyć na podstawie tych elementów:AHardwareBuffer

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

Na urządzeniach mobilnych, które obsługują współdziałanie OpenCL i OpenGL, bufory CL można tworzyć z buforów 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());

Przykładowe implementacje

Zapoznaj się z tymi implementacjami LiteRT w C++.

Podstawowe wnioskowanie (CPU)

Poniżej znajdziesz skróconą wersję fragmentów kodu z sekcji Rozpocznij. Jest to najprostsza implementacja wnioskowania za pomocą 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));

Kopiowanie zerowe z pamięcią hosta

Interfejs LiteRT Compiled Model API zmniejsza trudności związane z potokami wnioskowania, zwłaszcza w przypadku wielu backendów sprzętowych i przepływów bez kopiowania. Poniższy fragment kodu używa metody CreateFromHostMemory podczas tworzenia bufora wejściowego, który korzysta z kopiowania zerowego z pamięcią hosta.

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