使用 C++ 在 Android 上运行 LiteRT Next

LiteRT Next API 采用 C++ 编写,与 Kotlin API 相比,可让 Android 开发者更好地控制内存分配和低级开发。

如需查看 C++ 中的 LiteRT Next 应用示例,请参阅使用 C++ 进行异步分段演示

开始使用

请按以下步骤将 LiteRT Next 添加到您的 Android 应用。

更新 build 配置

使用 Bazel 使用 LiteRT 构建适用于 GPU、NPU 和 CPU 加速的 C++ 应用需要定义 cc_binary 规则,以确保编译、关联和打包所有必要的组件。通过以下示例设置,您的应用可以动态选择或利用 GPU、NPU 和 CPU 加速器。

以下是 Bazel 构建配置中的关键组件:

  • cc_binary 规则:这是用于定义 C++ 可执行目标(例如name = "your_application_name")。
  • srcs 属性:列出应用的 C++ 源文件(例如main.cc 和其他 .cc.h 文件)。
  • data 属性(运行时依赖项):对于打包应用在运行时加载的共享库和资源至关重要。
    • LiteRT 核心运行时:主要的 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(适用于 Android build)或 GLES 库(使用 gles_linkopts())。

以下是 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。以下代码会创建一个 Environment,用于在 CPU 和 GPU 上执行,不带任何选项:

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 Next API 的主要概念和组件,请参阅以下部分。

错误处理

LiteRT 使用 litert::Expected 返回值或传播错误的方式与 absl::StatusOrstd::expected 类似。您可以自行手动检查是否存在错误。

为方便起见,LiteRT 提供了以下宏:

  • 如果 expr 未产生错误,LITERT_ASSIGN_OR_RETURN(lhs, 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));

以下简化版代码段演示了 Compiled Model 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 Next 内置了对 I/O 缓冲区互操作性的支持,使用 Tensor Buffer API (TensorBuffer) 来处理数据在已编译模型中的流动。Tensor Buffer API 提供写入 (Write<T>()) 和读取 (Read<T>()) 功能,以及锁定 CPU 内存的功能。

如需更全面地了解 TensorBuffer API 的实现方式,请参阅 litert_tensor_buffer.h 的源代码。

查询模型输入/输出要求

分配张量缓冲区 (TensorBuffer) 的要求通常由硬件加速器指定。输入和输出的缓冲区可能对对齐、缓冲区步长和内存类型有要求。您可以使用 CreateInputBuffers 等辅助函数自动处理这些要求。

以下简化版代码段演示了如何检索输入数据的缓冲区要求:

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

如需更全面地了解 TensorBufferRequirements API 的实现方式,请参阅 litert_tensor_buffer_requirements.h 的源代码。

创建托管式张量缓冲区 (TensorBuffers)

以下简化版代码段演示了如何创建托管的 Tensor 缓冲区,其中 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 缓冲区

如需将现有缓冲区封装为张量缓冲区(零拷贝),请使用以下代码段:

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

您还可以通过 AHardwareBuffer 创建 OpenCL 缓冲区:

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 Next 实现。

基本推理(CPU)

以下是开始使用部分中代码段的浓缩版本。这是使用 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));

使用主机内存实现零拷贝

LiteRT Next 编译型模型 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);