在 Android 上使用 C++ 執行 LiteRT 編譯模型 API

LiteRT 編譯模型 API 提供 C++ 版本,可讓 Android 開發人員精細控管記憶體配置和低階開發作業。

如需 C++ 中的 LiteRT 應用程式範例,請參閱 C++ 示範的非同步區隔

開始使用

請按照下列步驟,將 LiteRT 編譯模型 API 新增至 Android 應用程式。

更新建構設定

使用 Bazel 建構 C++ 應用程式時,如要透過 GPU、NPU 和 CPU 加速 LiteRT,請定義 cc_binary 規則,確保所有必要元件都經過編譯、連結及封裝。以下範例設定可讓應用程式動態選擇或使用 GPU、NPU 和 CPU 加速器。

以下是 Bazel 建構設定的主要元件:

  • cc_binary 規則:這是用來定義 C++ 可執行目標的基本 Bazel 規則 (例如 name = "your_application_name")。
  • srcs 屬性:列出應用程式的 C++ 來源檔案 (例如main.cc,以及其他  或  檔案)。.cc.h
  • data 屬性 (執行階段依附元件):這項屬性對於封裝應用程式在執行階段載入的共用程式庫和資產至關重要。
    • LiteRT Core Runtime:主要的 LiteRT C API 共用程式庫 (例如//litert/c:litert_runtime_c_api_shared_lib)。
    • 調度程式庫:LiteRT 用於與硬體驅動程式通訊的供應商專屬共用程式庫 (例如//litert/vendors/qualcomm/dispatch:dispatch_api_so)。
    • GPU 後端程式庫:用於 GPU 加速的共用程式庫 (例如 "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so)。
    • NPU 後端程式庫:NPU 加速專用的特定共用程式庫,例如 Qualcomm 的 QNN HTP 程式庫 (例如@qairt//:lib/aarch64-android/libQnnHtp.so@qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so)。
    • 模型檔案和資產:訓練好的模型檔案、測試圖片、著色器,或執行階段所需的任何其他資料 (例如 :model_files:shader_files)。
  • deps 屬性 (編譯階段依附元件):列出程式碼需要編譯的程式庫。
    • LiteRT API 和公用程式:LiteRT 元件的標頭和靜態程式庫,例如張量緩衝區 (例如//litert/cc:litert_tensor_buffer)。
    • 圖形程式庫 (適用於 GPU):與圖形 API 相關的依附元件 (如果 GPU 加速器使用這些元件,例如 gles_deps())。
  • linkopts 屬性:指定傳遞至連結器的選項,可包含連結至系統程式庫 (例如 -landroid,或是使用 gles_linkopts() 的 GLES 程式庫)。

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
)

載入模型

取得 LiteRT 模型或將模型轉換為 .tflite 格式後,請建立 Model 物件來載入模型。

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

建立環境

Environment 物件提供執行階段環境,其中包含編譯器外掛程式路徑和 GPU 環境等元件。建立 CompiledModelTensorBuffer 時,必須提供 Environment。下列程式碼會建立 CPU 和 GPU 執行作業的 Environment,不含任何選項:

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

建立已編譯的模型

使用 CompiledModel API,透過新建立的 Model 物件初始化執行階段。您可以在這個時間點指定硬體加速 (kLiteRtHwAcceleratorCpukLiteRtHwAcceleratorGpu):

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

建立輸入和輸出緩衝區

建立必要的資料結構 (緩衝區),以保存您將輸入模型進行推論的資料,以及模型在執行推論後產生的輸出資料。

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

如果您使用 CPU 記憶體,請直接將資料寫入第一個輸入緩衝區,填入輸入內容。

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

叫用模型

提供輸入和輸出緩衝區,然後使用在上一個步驟中指定的模型和硬體加速功能,執行已編譯的模型。

compiled_model.Run(input_buffers, output_buffers);

擷取輸出內容

直接從記憶體讀取模型輸出,即可擷取輸出內容。

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

重要概念和元件

如要瞭解 LiteRT 編譯模型 API 的重要概念和元件,請參閱下列各節。

處理錯誤

LiteRT 會使用 litert::Expected 傳回值或傳播錯誤,方式與 absl::StatusOrstd::expected 類似。您可以自行手動檢查錯誤。

為方便起見,LiteRT 提供下列巨集:

  • 如果 expr 未產生錯誤,LITERT_ASSIGN_OR_RETURN(lhs, expr) 會將 expr 的結果指派給 lhs,否則會傳回錯誤。

    展開後會類似下列程式碼片段。

    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) 的功能與 LITERT_ASSIGN_OR_RETURN 相同,但如果發生錯誤,會中止程式。

  • 如果 LITERT_RETURN_IF_ERROR(expr) 的評估結果為錯誤,則會傳回 expr

  • LITERT_ABORT_IF_ERROR(expr) 的功能與 LITERT_RETURN_IF_ERROR 相同,但如果發生錯誤,會中止程式。

如要進一步瞭解 LiteRT 巨集,請參閱 litert_macros.h

已編譯模型 (CompiledModel)

編譯模型 API (CompiledModel) 負責載入模型、套用硬體加速、例項化執行階段、建立輸入和輸出緩衝區,以及執行推論。

下列簡化程式碼片段示範 Compiled Model API 如何採用 LiteRT 模型 (.tflite) 和目標硬體加速器 (GPU),並建立已編譯的模型,準備執行推論。

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

下列簡化程式碼片段說明編譯模型 API 如何接收輸入和輸出緩衝區,並使用編譯模型執行推論。

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

如要更全面瞭解 CompiledModel API 的實作方式,請參閱 litert_compiled_model.h 的原始碼。

張量緩衝區 (TensorBuffer)

LiteRT 內建支援 I/O 緩衝區互通性,並使用 Tensor Buffer API (TensorBuffer) 處理編譯模型內外的資料流。Tensor Buffer API 可用於寫入 (Write<T>()) 和讀取 (Read<T>()),以及鎖定 CPU 記憶體。

如要更全面地瞭解如何實作 TensorBuffer API,請參閱 litert_tensor_buffer.h 的原始碼。

查詢模型輸入/輸出規定

配置 Tensor 緩衝區 (TensorBuffer) 的需求通常由硬體加速器指定。輸入和輸出緩衝區可能會有對齊、緩衝區步幅和記憶體類型方面的要求。您可以使用 CreateInputBuffers 等輔助函式,自動處理這些需求。

下列簡化程式碼片段示範如何擷取輸入資料的緩衝區需求:

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

如要更全面地瞭解如何實作 TensorBufferRequirements API,請參閱 litert_tensor_buffer_requirements.h 的原始碼。

建立受管理張量緩衝區 (TensorBuffers)

下列簡化程式碼片段示範如何建立 Managed Tensor Buffers,其中 TensorBuffer API 會分配相應的緩衝區:

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

如要將現有緩衝區包裝為 Tensor 緩衝區 (零複製),請使用下列程式碼片段:

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

從 Tensor 緩衝區讀取及寫入資料

下列程式碼片段示範如何從輸入緩衝區讀取資料,以及如何寫入輸出緩衝區:

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

進階:適用於特殊硬體緩衝區類型的零複製緩衝區互通性

某些緩衝區類型 (例如 AHardwareBuffer) 可與其他緩衝區類型互通。舉例來說,您可以從 AHardwareBuffer 建立 OpenGL 緩衝區,並採用零複製方式。以下程式碼片段提供範例:

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 緩衝區:AHardwareBuffer

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

在支援 OpenCL 和 OpenGL 互通性的行動裝置上,可以從 GL 緩衝區建立 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());

實作範例

請參閱下列 C++ 中的 LiteRT 實作項目。

基本推論 (CPU)

以下是「開始使用」一節中程式碼片段的簡化版本。這是使用 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));

使用主機記憶體的零複製

LiteRT 編譯模型 API 可減少推論管道的摩擦,特別是在處理多個硬體後端和零複製流程時。下列程式碼片段會在建立輸入緩衝區時使用 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);