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++ 実行可能ターゲットの定義に使用される基本的な Bazel ルールです(name = "your_application_name")を指定できます。
  • srcs 属性: アプリケーションの C++ ソースファイル(例:main.cc や他の .cc ファイル、.h ファイルなど)。
  • data 属性(ランタイム依存関係): アプリケーションがランタイムで読み込む共有ライブラリとアセットをパッケージ化する場合、これは重要です。
    • LiteRT Core ランタイム: メインの 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 ライブラリなど、NPU アクセラレーション用の特定の共有ライブラリ(@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 を使用する場合のグラフィック API 関連の依存関係(gles_deps())。
  • linkopts 属性: リンカーに渡されるオプションを指定します。これには、システム ライブラリ(-landroid(Android ビルドの場合)または 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 コンテキストなどのコンポーネントを含むランタイム環境を提供します。Environment は、CompiledModelTensorBuffer を作成する場合に必要です。次のコードは、オプションなしで 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 は、absl::StatusOrstd::expected と同様に、litert::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)

Compiled Model 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 には、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 がそれぞれのバッファを割り当てる、マネージド テンソル バッファを作成する方法を示しています。

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(ゼロコピー)としてラップするには、次のコード スニペットを使用します。

// 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 バッファはゼロコピーで 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());

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