C++로 Android에서 LiteRT Next 실행

LiteRT Next API는 C++에서 사용할 수 있으며 Android 개발자가 Kotlin API보다 메모리 할당 및 하위 수준 개발을 더 효과적으로 제어할 수 있습니다.

C++의 LiteRT Next 애플리케이션 예시는 C++ 데모를 사용한 비동기 세분화를 참고하세요.

시작하기

Android 애플리케이션에 LiteRT Next를 추가하려면 다음 단계를 따르세요.

빌드 구성 업데이트

Bazel을 사용하여 GPU, NPU, CPU 가속을 위한 LiteRT로 C++ 애플리케이션을 빌드하려면 필요한 모든 구성요소가 컴파일, 연결, 패키징되도록 cc_binary 규칙을 정의해야 합니다. 다음 설정 예시를 사용하면 애플리케이션에서 GPU, NPU, CPU 가속기를 동적으로 선택하거나 활용할 수 있습니다.

Bazel 빌드 구성의 주요 구성요소는 다음과 같습니다.

  • cc_binary 규칙: C++ 실행 파일 타겟 (예: 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 백엔드 라이브러리: 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용): GPU 가속기에서 사용하는 경우 그래픽 API와 관련된 종속 항목입니다 (예: gles_deps()))를 제공합니다.
  • linkopts 속성: 링커에 전달되는 옵션을 지정합니다. 여기에는 시스템 라이브러리 (예: Android 빌드의 경우 -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 컨텍스트의 경로와 같은 구성요소가 포함된 런타임 환경을 제공합니다. EnvironmentCompiledModelTensorBuffer를 만들 때 필요합니다. 다음 코드는 옵션 없이 CPU 및 GPU 실행을 위한 Environment를 만듭니다.

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

컴파일된 모델 만들기

CompiledModel API를 사용하여 새로 만든 Model 객체로 런타임을 초기화합니다. 이 지점에서 하드웨어 가속(kLiteRtHwAcceleratorCpu 또는 kLiteRtHwAcceleratorGpu)을 지정할 수 있습니다.

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::StatusOr 또는 std::expected와 유사한 방식으로 값을 반환하거나 오류를 전파합니다. 오류를 직접 수동으로 확인할 수 있습니다.

편의를 위해 LiteRT는 다음 매크로를 제공합니다.

  • 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)는 모델을 로드하고, 하드웨어 가속을 적용하고, 런타임을 인스턴스화하고, 입력 및 출력 버퍼를 만들고, 추론을 실행합니다.

다음의 단순화된 코드 스니펫은 컴파일된 모델 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 Next는 Tensor Buffer API (TensorBuffer)를 사용하여 컴파일된 모델의 데이터 흐름을 처리하는 I/O 버퍼 상호 운용성을 위한 기본 제공 지원을 제공합니다. 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) 만들기

다음의 단순화된 코드 스니펫은 TensorBuffer API가 각 버퍼를 할당하는 관리형 Tensor Buffers를 만드는 방법을 보여줍니다.

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와 같은 특정 버퍼 유형은 다른 버퍼 유형과의 상호 운용성을 허용합니다. 예를 들어 OpenGL 버퍼는 0 복사로 AHardwareBuffer에서 만들 수 있습니다. 다음 코드 스니펫은 예를 보여줍니다.

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