Chạy LiteRT Compiled Model API trên Android bằng C++

Các API Mô hình đã biên dịch LiteRT có trong C++, giúp nhà phát triển Android kiểm soát chi tiết việc phân bổ bộ nhớ và quá trình phát triển cấp thấp.

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

Bắt đầu

Hãy làm theo các bước sau để thêm LiteRT Compiled Model API vào ứng dụng Android của bạn.

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

Việc tạo một ứ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ả các thành phần cần thiết đều được biên dịch, liên kết và đóng gói. Chế độ thiết lập ví dụ sau đây cho phép ứng dụng của bạn linh hoạt chọn hoặc sử dụng các bộ 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:

  • cc_binary Quy tắc: Đâ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").
  • srcs Thuộc tính: 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).
  • data Thuộc tính (Phần phụ thuộc thời gian chạy): Thuộc tính này rất quan trọng đối với việc đóng gói các thư viện và thành phần dùng chung mà ứng dụng của bạn tải trong thời gian chạy.
    • Thời gian chạy LiteRT Core: Thư viện dùng chung chính của LiteRT C API (ví dụ: //litert/c:litert_runtime_c_api_shared_lib).
    • Thư viện điều phối: Thư viện chia sẻ dành riêng cho nhà cung cấp mà LiteRT 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 và tài sản mô hình: Tệp mô hình đã huấn luyện, hình ảnh kiểm thử, chương trình đổ bóng hoặc mọi dữ liệu 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 trong thời gian 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.
    • Tiện ích và API LiteRT: Tiêu đề và thư viện tĩnh cho các thành phần LiteRT như bộ đệm tensor (ví dụ: //litert/cc:litert_tensor_buffer).
    • Thư viện đồ hoạ (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 API đó (ví dụ: gles_deps()).
  • linkopts Thuộc tính: Chỉ định các lựa chọn được truyền đến trình liên kết, có thể bao gồm cả 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 có gles_linkopts()).

Sau đây là ví dụ về một 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 nhận được một mô hình LiteRT hoặc chuyển đổi một mô hình sang định dạng .tflite, hãy tải mô hình bằng cách tạo một đố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 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ó lựa 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 động 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 (vùng đệm) cần thiết để 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 các vùng đệm đầu vào và đầu ra, chạy Mô hình đã biên dịch với 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 dữ liệu đầu ra

Truy xuất đầu ra bằng cách đọc trực tiếp đầ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 Mô hình đã biên dịch LiteRT.

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) chỉ định kết quả của expr cho lhs nếu không tạo ra lỗi, nếu không, hàm này sẽ trả về lỗi.

    Nội dung này sẽ mở rộng thành một đoạn trích tương tự 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 sẽ huỷ chương trình trong trường hợp xảy ra lỗi.

  • LITERT_RETURN_IF_ERROR(expr) sẽ 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 đã biên dịch (CompiledModel)

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

Đoạn mã đơn giản sau đây minh hoạ cách Compiled Model API lấy một mô hình LiteRT (.tflite) và bộ 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 Compiled Model API nhận một 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)));

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

Vùng đệm tensor (TensorBuffer)

LiteRT cung cấp khả năng hỗ trợ tích hợp cho khả năng tương tác của bộ đệm I/O, sử dụng Tensor Buffer API (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.

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

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

Các yêu cầu để phân bổ một Tensor Buffer (TensorBuffer) thường do bộ 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 về việc căn chỉnh, bước vùng đệm và loại bộ nhớ. Bạn có thể 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 cho litert_tensor_buffer_requirements.h.

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

Đoạn mã đơn giản sau đây minh hoạ cách tạo Managed Tensor Buffer, 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 Tensor Buffer bằng tính năng sao chép bằng không

Để bao bọc một vùng đệm hiện có dưới dạng Vùng đệm Tensor (sao chép bằng 0), 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: Khả năng tương tác với vùng đệm sao chép bằng 0 cho các loại vùng đệm phần cứng chuyên dụng

Một số loại vùng đệm, 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 một vùng đệm OpenGL từ AHardwareBuffer mà không cần 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 các vùng đệm CL từ các 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 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.

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

Sao chép không cần bộ nhớ máy chủ

API Mô hình đã biên dịch LiteRT giúp giảm ma sát của các quy trình suy luận, đặc biệt là khi xử lý nhiều phần phụ trợ phần cứng và các luồng sao chép bằng 0. Đ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 sao chép bằng 0 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);