Chạy LiteRT Next trên Android bằng C++

API LiteRT Next có sẵn trong C++ và có thể cung cấp cho nhà phát triển Android nhiều quyền kiểm soát hơn đối với việc phân bổ bộ nhớ và phát triển cấp thấp so với API Kotlin.

Để biết ví dụ về ứng dụng LiteRT Next trong C++, hãy xem phần Phân đoạn không đồng bộ bằng bản minh hoạ C++.

Bắt đầu

Hãy làm theo các bước sau để thêm LiteRT Next vào ứng dụng Android.

Cập nhật cấu hình bản dựng

Việc tạo ứng dụng C++ bằng LiteRT để tăng tốc GPU, NPU và CPU bằng Bazel liên quan đến việc xác định quy tắc cc_binary để đảm bảo tất cả thành phần cần thiết đều được biên dịch, liên kết và đóng gói. Cấu hình ví dụ sau đây cho phép ứng dụng của bạn linh động chọn hoặc sử dụng trình tăng tốc GPU, NPU và CPU.

Sau đây là các thành phần chính trong cấu hình bản dựng Bazel:

  • Quy tắc cc_binary: Đây là quy tắc cơ bản của Bazel dùng để xác định mục tiêu thực thi C++ (ví dụ: name = "your_application_name").
  • Thuộc tính srcs: Liệt kê các tệp nguồn C++ của ứng dụng (ví dụ: main.cc và các tệp .cc hoặc .h khác).
  • Thuộc tính data (Phần phụ thuộc thời gian chạy): Đây là thuộc tính quan trọng để đóng gói các thư viện và tài sản dùng chung mà ứng dụng của bạn tải trong thời gian chạy.
    • LiteRT Core Runtime: Thư viện dùng chung API LiteRT C chính (ví dụ: //litert/c:litert_runtime_c_api_shared_lib).
    • Thư viện điều phối: Thư viện dùng chung dành riêng cho nhà cung cấp mà LiteRT sử dụng để giao tiếp với trình điều khiển phần cứng (ví dụ: //litert/vendors/qualcomm/dispatch:dispatch_api_so).
    • Thư viện phụ trợ GPU: Thư viện dùng chung để tăng tốc GPU (ví dụ: "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so).
    • Thư viện phụ trợ NPU: Các thư viện dùng chung cụ thể để tăng tốc NPU, chẳng hạn như thư viện QNN HTP của Qualcomm (ví dụ: @qairt//:lib/aarch64-android/libQnnHtp.so, @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so).
    • Tệp mô hình và tài sản: Tệp mô hình đã huấn luyện, hình ảnh kiểm thử, chương trình đổ bóng hoặc bất kỳ dữ liệu nào khác cần thiết trong thời gian chạy (ví dụ: :model_files, :shader_files).
  • Thuộc tính deps (Phần phụ thuộc tại thời điểm biên dịch): Thuộc tính này liệt kê các thư viện mà mã của bạn cần biên dịch.
    • API và tiện ích LiteRT: Tiêu đề và thư viện tĩnh cho các thành phần LiteRT như vùng đệm tensor (ví dụ: //litert/cc:litert_tensor_buffer).
    • Thư viện đồ hoạ (dành cho GPU): Các phần phụ thuộc liên quan đến API đồ hoạ nếu trình tăng tốc GPU sử dụng các phần phụ thuộc đó (ví dụ: gles_deps()).
  • Thuộc tính linkopts: Chỉ định các tuỳ chọn được truyền đến trình liên kết, có thể bao gồm việc liên kết với các thư viện hệ thống (ví dụ: -landroid cho các bản dựng Android hoặc thư viện GLES với gles_linkopts()).

Sau đây là ví dụ về quy tắc 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
)

Tải mô hình

Sau khi lấy mô hình LiteRT hoặc chuyển đổi mô hình sang định dạng .tflite, hãy tải mô hình bằng cách tạo đối tượng Model.

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

Tạo môi trường

Đối tượng Environment cung cấp một môi trường thời gian chạy bao gồm các thành phần như đường dẫn của trình bổ trợ trình biên dịch và ngữ cảnh GPU. Bạn bắt buộc phải có Environment khi tạo CompiledModelTensorBuffer. Đoạn mã sau đây tạo một Environment để thực thi CPU và GPU mà không có bất kỳ tuỳ chọn nào:

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

Tạo mô hình đã biên dịch

Sử dụng API CompiledModel, hãy khởi chạy thời gian chạy bằng đối tượng Model mới tạo. Bạn có thể chỉ định tính năng tăng tốc phần cứng tại thời điểm này (kLiteRtHwAcceleratorCpu hoặc kLiteRtHwAcceleratorGpu):

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

Tạo vùng đệm đầu vào và đầu ra

Tạo các cấu trúc dữ liệu cần thiết (vùng đệm) để lưu trữ dữ liệu đầu vào mà bạn sẽ đưa vào mô hình để suy luận và dữ liệu đầu ra mà mô hình tạo ra sau khi chạy suy luận.

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

Nếu bạn đang sử dụng bộ nhớ CPU, hãy điền dữ liệu đầu vào bằng cách ghi dữ liệu trực tiếp vào vùng đệm đầu vào đầu tiên.

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

Gọi mô hình

Cung cấp vùng đệm đầu vào và đầu ra, chạy Mô hình đã biên dịch bằng mô hình và tính năng tăng tốc phần cứng được chỉ định ở các bước trước.

compiled_model.Run(input_buffers, output_buffers);

Truy xuất đầu ra

Truy xuất đầu ra bằng cách trực tiếp đọc đầu ra của mô hình từ bộ nhớ.

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

Các khái niệm và thành phần chính

Hãy tham khảo các phần sau để biết thông tin về các khái niệm và thành phần chính của API LiteRT Next.

Xử lý lỗi

LiteRT sử dụng litert::Expected để trả về giá trị hoặc truyền lỗi theo cách tương tự như absl::StatusOr hoặc std::expected. Bạn có thể tự kiểm tra lỗi theo cách thủ công.

Để thuận tiện, LiteRT cung cấp các macro sau:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) gán kết quả của expr cho lhs nếu kết quả không tạo ra lỗi và trả về lỗi nếu kết quả tạo ra lỗi.

    Mã này sẽ mở rộng thành một đoạn mã như sau.

    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) hoạt động giống như LITERT_ASSIGN_OR_RETURN nhưng huỷ chương trình trong trường hợp xảy ra lỗi.

  • LITERT_RETURN_IF_ERROR(expr) trả về expr nếu quá trình đánh giá của hàm này tạo ra lỗi.

  • LITERT_ABORT_IF_ERROR(expr) hoạt động giống như LITERT_RETURN_IF_ERROR nhưng sẽ huỷ chương trình trong trường hợp xảy ra lỗi.

Để biết thêm thông tin về macro LiteRT, hãy xem litert_macros.h.

Mô hình được biên dịch (CompiledModel)

API mô hình đã biên dịch (CompiledModel) chịu trách nhiệm tải mô hình, áp dụng tính năng tăng tốc phần cứng, tạo bản sao thời gian chạy, tạo bộ đệm đầu vào và đầu ra, cũng như chạy quy trình suy luận.

Đoạn mã đơn giản sau đây minh hoạ cách API mô hình đã biên dịch lấy một mô hình LiteRT (.tflite) và trình tăng tốc phần cứng mục tiêu (GPU), đồng thời tạo một mô hình đã biên dịch sẵn sàng chạy suy luận.

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

Đoạn mã đơn giản sau đây minh hoạ cách API mô hình đã biên dịch lấy vùng đệm đầu vào và đầu ra, đồng thời chạy các suy luận bằng mô hình đã biên dịch.

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

Để xem toàn diện hơn về cách triển khai API CompiledModel, hãy xem mã nguồn của litert_compiled_model.h.

Vùng đệm tensor (TensorBuffer)

LiteRT Next tích hợp sẵn tính năng hỗ trợ khả năng tương tác của vùng đệm I/O, sử dụng API Vùng đệm tensor (TensorBuffer) để xử lý luồng dữ liệu vào và ra khỏi mô hình đã biên dịch. Tensor Buffer API cung cấp khả năng ghi (Write<T>()) và đọc (Read<T>()), đồng thời khoá bộ nhớ CPU.

Để xem thông tin đầy đủ hơn về cách triển khai API TensorBuffer, hãy xem mã nguồn của litert_tensor_buffer.h.

Yêu cầu về đầu vào/đầu ra của mô hình truy vấn

Yêu cầu để phân bổ Vùng đệm tensor (TensorBuffer) thường do trình tăng tốc phần cứng chỉ định. Vùng đệm cho dữ liệu đầu vào và đầu ra có thể có các yêu cầu liên quan đến căn chỉnh, bước đệm và loại bộ nhớ. Bạn có thể sử dụng các hàm trợ giúp như CreateInputBuffers để tự động xử lý các yêu cầu này.

Đoạn mã đơn giản sau đây minh hoạ cách bạn có thể truy xuất các yêu cầu về vùng đệm cho dữ liệu đầu vào:

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

Để biết thông tin đầy đủ hơn về cách triển khai API TensorBufferRequirements, hãy xem mã nguồn của litert_tensor_buffer_requirements.h.

Tạo vùng đệm tensor được quản lý (TensorBuffers)

Đoạn mã đơn giản sau đây minh hoạ cách tạo Vùng đệm tensor được quản lý, trong đó API TensorBuffer phân bổ các vùng đệm tương ứng:

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

Tạo vùng đệm Tensor không sao chép

Để gói một vùng đệm hiện có dưới dạng Vùng đệm Tensor (không sao chép), hãy sử dụng đoạn mã sau:

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

Đọc và ghi từ Vùng đệm tensor

Đoạn mã sau đây minh hoạ cách bạn có thể đọc từ vùng đệm đầu vào và ghi vào vùng đệm đầu ra:

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

Nâng cao: Tương tác vùng đệm không sao chép cho các loại vùng đệm phần cứng chuyên biệt

Một số loại vùng đệm nhất định, chẳng hạn như AHardwareBuffer, cho phép khả năng tương tác với các loại vùng đệm khác. Ví dụ: bạn có thể tạo vùng đệm OpenGL từ một AHardwareBuffer không sao chép. Đoạn mã sau đây cho thấy một ví dụ:

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

Bạn cũng có thể tạo vùng đệm OpenCL từ AHardwareBuffer:

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

Trên các thiết bị di động hỗ trợ khả năng tương tác giữa OpenCL và OpenGL, bạn có thể tạo vùng đệm CL từ vùng đệm 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());

Ví dụ về cách triển khai

Hãy tham khảo các cách triển khai LiteRT Next sau đây trong C++.

Suy luận cơ bản (CPU)

Sau đây là phiên bản rút gọn của các đoạn mã trong phần Bắt đầu. Đây là cách triển khai suy luận đơn giản nhất bằng 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));

Không sao chép với bộ nhớ máy chủ

API mô hình biên dịch LiteRT Next giúp giảm sự cố trong quy trình suy luận, đặc biệt là khi xử lý nhiều phần phụ trợ phần cứng và luồng không sao chép. Đoạn mã sau đây sử dụng phương thức CreateFromHostMemory khi tạo vùng đệm đầu vào, sử dụng tính năng không sao chép với bộ nhớ máy chủ.

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