Interfejsy LiteRT Next są dostępne w C++ i mogą zapewnić deweloperom Androida większą kontrolę nad przydziałem pamięci i rozwojem na niskim poziomie niż interfejsy API w Kotlinie.
Przykład aplikacji LiteRT Next w C++ znajdziesz w demonstracji asynchronicznego podziału na segmenty za pomocą C++.
Rozpocznij
Aby dodać LiteRT Next do aplikacji na Androida, wykonaj te czynności.
Aktualizowanie konfiguracji kompilacji
Tworzenie aplikacji C++ z LiteRT do przyspieszania GPU, NPU i CPU za pomocą Bazel wymaga zdefiniowania reguły cc_binary
, aby zapewnić skompilowanie, połączenie i zapakowanie wszystkich niezbędnych komponentów. Na przykład konfiguracja przedstawiona poniżej pozwala aplikacji dynamicznie wybierać lub wykorzystywać akceleratory GPU, NPU i CPU.
Oto kluczowe komponenty w konfiguracji kompilacji Bazel:
cc_binary
Reguła: to podstawowa reguła Bazel używana do definiowania docelowego pliku wykonywalnego C++ (np.name = "your_application_name"
).- Atrybut
srcs
: lista plików źródłowych C++ aplikacji (np.main.cc
i inne pliki.cc
lub.h
). data
Atrybut (zależność w czasie wykonywania): jest to ważne w przypadku pakowania bibliotek współdzielonych i komponentów, które aplikacja wczytuje w czasie wykonywania.- LiteRT Core Runtime: główna biblioteka LiteRT C API (np.
//litert/c:litert_runtime_c_api_shared_lib
). - Biblioteki rozsyłania: biblioteki wspólne konkretnego dostawcy, których LiteRT używa do komunikacji z sterownikami sprzętowymi (np.
//litert/vendors/qualcomm/dispatch:dispatch_api_so
). - Biblioteki backendu GPU: wspólne biblioteki do akceleracji GPU (np.
"@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so
). - Biblioteki backendu NPU: konkretne biblioteki współdzielone do przyspieszania NPU, takie jak 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 wykonywania (np.
:model_files
,:shader_files
).
- LiteRT Core Runtime: główna biblioteka LiteRT C API (np.
deps
Atrybut (zależność kompilacji): zawiera listę bibliotek, których potrzebuje kod do skompilowania.- Interfejsy API i narzędzia LiteRT: nagłówki i biblioteki statyczne komponentów LiteRT, takie 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()
).
- Interfejsy API i narzędzia LiteRT: nagłówki i biblioteki statyczne komponentów LiteRT, takie jak bufory tensorów (np.
- Atrybut
linkopts
: określa opcje przekazywane linkerowi, które mogą obejmować łączenie z bibliotekami systemowymi (np.-landroid
w przypadku wersji na Androida lub bibliotek GLES zgles_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
)
Wczytaj model
Po uzyskaniu modelu LiteRT lub przekonwertowaniu modelu na format .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 obejmuje komponenty takie jak ścieżka wtyczki kompilatora i konteksty GPU. Właściwość Environment
jest wymagana podczas tworzenia właściwości CompiledModel
i TensorBuffer
. Ten kod tworzy Environment
do wykonania na procesorze CPU i GPU bez żadnych opcji:
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
Tworzenie skompilowanego modelu
Za pomocą interfejsu API CompiledModel
zainicjuj środowisko uruchomieniowe za pomocą nowo utworzonego obiektu Model
. W tym miejscu 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ędą podawane do modelu na potrzeby wnioskowania, oraz danych wyjściowych, które model wygeneruje po przeprowadzeniu 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 dane, zapisując je bezpośrednio w pierwszym buforze danych wejściowych.
input_buffers[0].Write<float>(absl::MakeConstSpan(input_data, input_size));
Wywoływanie modelu
Przekazanie buforów wejściowych i wyjściowych oraz uruchomienie skompilowanego modelu z akceleracją sprzętową zgodnie z opisem w poprzednich krokach.
compiled_model.Run(input_buffers, output_buffers);
Pobieranie danych wyjściowych
Pobieranie danych wyjściowych przez bezpośrednie odczytanie danych wyjściowych modelu 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 najważniejszych pojęciach i komponentach interfejsów LiteRT Next.
Obsługa błędów
Funkcja LiteRT używa argumentu litert::Expected
do zwracania wartości lub rozpowszechniania błędów w podobny sposób do funkcji absl::StatusOr
lub std::expected
. Możesz samodzielnie sprawdzić, czy nie ma błędu.
Dla ułatwienia LiteRT udostępnia te makro:
Funkcja
LITERT_ASSIGN_OR_RETURN(lhs, expr)
przypisuje wynik funkcjiexpr
do zmiennejlhs
, jeśli nie zwraca ona błędu. W przeciwnym razie zwraca błąd.Rozwinie się do tego fragmentu.
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 jakLITERT_ASSIGN_OR_RETURN
, ale w przypadku błędu powoduje przerwanie programu.Funkcja
LITERT_RETURN_IF_ERROR(expr)
zwracaexpr
, jeśli jej ocena powoduje błąd.LITERT_ABORT_IF_ERROR(expr)
działa tak samo jakLITERT_RETURN_IF_ERROR
, ale w przypadku błędu przerywa program.
Więcej informacji o makrach LiteRT znajdziesz w artykule litert_macros.h
.
Kompilowany model (CompiledModel)
Interfejs skompilowanego modelu (CompiledModel
) odpowiada za wczytywanie modelu, stosowanie przyspieszenia sprzętowego, tworzenie instancji środowiska uruchomieniowego, tworzenie buforów wejściowych i wyjściowych oraz wykonywanie wnioskowania.
Ten uproszczony fragment kodu pokazuje, jak interfejs skompilowanego modelu API pobiera model LiteRT (.tflite
) i docelowy akcelerator sprzętowy (GPU), a następnie tworzy skompilowany model, który jest gotowy do wykonywania 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));
Ten uproszczony fragment kodu pokazuje, jak interfejs API skompilowanego modelu pobiera bufor wejściowy i wyjściowy oraz wykonuje 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łny wgląd w to, jak jest implementowany interfejs API CompiledModel
, zapoznaj się ze źródłowym kodem pliku litert_compiled_model.h.
Tensor Buffer (TensorBuffer)
LiteRT Next zapewnia wbudowane wsparcie dla interoperacyjności bufora wejścia/wyjścia, używając interfejsu Tensor Buffer API (TensorBuffer
) do obsługi przepływu danych do i z skompilowanego modelu. Interfejs Tensor Buffer API umożliwia zapisywanie danych (Write<T>()
) i ich odczyt (Read<T>()
) oraz blokowanie pamięci procesora.
Aby uzyskać pełny wgląd w to, jak jest implementowany interfejs TensorBuffer
API, zapoznaj się z kodem źródłowym pliku litert_tensor_buffer.h.
Wymagania dotyczące danych wejściowych i wyjściowych modelu zapytań
Wymagania dotyczące przydzielenia 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, kroku buforowego i typu pamięci. Aby automatycznie obsługiwać te wymagania, możesz użyć funkcji pomocniczych, takich jak CreateInputBuffers
.
Ten uproszczony fragment kodu pokazuje, jak pobrać wymagania dotyczące bufora dla danych wejściowych:
LITERT_ASSIGN_OR_RETURN(auto reqs, compiled_model.GetInputBufferRequirements(signature_index, input_index));
Aby uzyskać pełny wgląd w to, jak jest implementowany interfejs TensorBufferRequirements
, zapoznaj się z kodem źródłowym pliku litert_tensor_buffer_requirements.h.
Tworzenie zarządzanych buforów tensorów (TensorBuffers)
Ten uproszczony fragment kodu pokazuje, jak tworzyć zarządzane bufory tensorów, w których interfejs API TensorBuffer
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 tensorów z mechanizmem zero-copy
Aby owinąć 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 z bufora tensora
Ten fragment kodu pokazuje, jak odczytać dane z bufora wejściowego i zapisać do bufora wyjściowego:
// 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: współdziałanie buforów typu zero-copy ze specjalistycznymi typami 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że zostać utworzony z poziomu AHardwareBuffer
bez kopiowania. Przykładowy fragment kodu:
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ć z tych elementów: AHardwareBuffer
:
LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_ahwb.GetOpenClMemory());
Na urządzeniach mobilnych, które obsługują interoperacyjność między OpenCL i OpenGL, z buforów GL można tworzyć bufory CL:
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 Next w C++.
Podstawowe wnioskowanie (CPU)
Poniżej znajduje się skrócona wersja fragmentów kodu z sekcji Rozpoczęcie. Jest to najprostsza implementacja wnioskowania za pomocą 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));
Kopiowanie bez udziału użytkownika z pamięcią hosta
Interfejs LiteRT Next Compiled Model API zmniejsza obciążenie systemów przetwarzania, zwłaszcza w przypadku korzystania z wielu backendów sprzętowych i przepływów typu zero-copy. Podany niżej fragment kodu podczas tworzenia bufora wejściowego, który korzysta z metody zero-copy z pamięcią hosta, używa metody CreateFromHostMemory
.
// 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);