mcpField
L
litert-runtime

litert-runtime

github.com/android開発10.0k213 回インストール2 日前に更新
リポジトリ

litert-runtime について

Kotlin Android 開発者向けに、LiteRT CompiledModel で `.tflite` モデルを CPU または GPU で実行する手順を示すコーディングスキルです。スレッド管理、バッファの扱い、旧 Interpreter API からの移行を重視します。

主な機能

  1. 01LiteRT CompiledModel の設定
  2. 02CPU と GPU の推論ガイド
  3. 03単一スレッドでのモデル管理
  4. 04テンソルバッファの再利用
  5. 05旧 Interpreter API からの移行

活用シーン

  1. 01Android アプリへの端末内 `.tflite` 推論の追加
  2. 02旧 Interpreter API からのアプリ移行
  3. 03GPU フォールバックと推論時間の処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/android/skills --skill litert-runtime

インストール後、Claude Code で/litert-runtimeを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
migration.md
verify.md
references/android/google/play/
on-device-ai.md

詳細

このスキルでは、LiteRT CompiledModel API(com.google.ai.edge.litert:litert 2.x、ソースリポジトリ)を使用して、CPU または GPU で .tflite モデルを実行する Android アプリを構築する手順を説明します。非推奨の org.tensorflow.lite.Interpreter API は使用しないでください。.litertlm ファイルを使用するオープンなテキスト言語モデルには、litert-lm スキルを提案してください。

前提条件

  • Kotlin の Android プロジェクト(LiteRT 2.3.0 AAR で宣言されているとおり、minSdk は 24 以上である必要があります)。
  • Google Maven から取得する、アプリレベルの build.gradle.kts ファイルの依存関係:implementation("com.google.ai.edge.litert:litert:2.3.0") と implementation("com.google.ai.edge.litert:litert-gpu:2.3.0")。2.3.0 では GPU アクセラレーターは 2 つ目のアーティファクトです。litert のみではアプリはビルドされますが、APK に GPU アクセラレーターライブラリが含まれないため、CompiledModel.Options(Accelerator.GPU) を指定した CompiledModel.create は LiteRtException をスローします。CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) では例外はスローされません。アプリは GPU を報告しながら、モデルは CPU で実行され、Logcat にのみその旨(GPU accelerator could not be loaded and registered)が表示されます。マニフェストへのエントリーは不要です。litert が依存する litert-api が、独自のマニフェストで OpenCL ドライバーライブラリ(uses-native-library)を宣言しています。
  • 検証済みの .tflite モデルと、その入力仕様(入力の次元、平均値、標準偏差、チャンネル順序、テンソルレイアウト)。検証済みのレシピがあるモデルについては、LiteRT model recipes を確認してください。

重要な必須事項

  • 単一スレッドへの閉じ込め:単一スレッドのエグゼキューターまたは閉じ込められたコルーチンディスパッチャーが、CompiledModel インスタンスを所有する必要があります。作成、推論、クローズはすべて、メインスレッドではなく、同じバックグラウンドスレッドで実行してください。
  • バッファーのライフサイクル:createInputBuffers と createOutputBuffers を使用して TensorBuffer インスタンスを一度だけ割り当て、推論のたびに再利用してください。CompiledModel をクローズする前に、すべての入力バッファーと出力バッファーをクローズしてください。
  • 同期的な計測:model.run は GPU に処理をキューイングし、GPU の実行が完了する前に戻ります。必ず model.run と出力バッファーの読み戻し(outputs[0].readFloat)を合わせて計測してください。
  • ウォームアップと例外安全性:初回の GPU シェーダーコンパイルでユーザーの最初の操作がブロックされないよう、ヘルパーコンストラクターの init ブロック内でウォームアップ推論を 1 回実行してください。また、バッファーの割り当てやウォームアップで例外がスローされた場合に、CompiledModel と TensorBuffer のハンドルがリークしないよう、例外を再スローする前に catch 内で close を呼び出してください。
  • 移行:アプリがすでに org.tensorflow.lite のコードを使用している場合、ファイルを編集する前に「Migrating from legacy TFLite Interpreter」セクションを全文読む必要があります。移行後のコードでは、DataType を含む org.tensorflow.lite のクラスをインポートしてはいけません。GpuDelegate は CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) に置き換え、NNAPI の選択も同じ GPU オプションに対応させてください。

詳細な手順

ステップ 1:Gradle を設定し、モデルファイルを配置する

ビルドファイルを編集する前に、プロジェクト内で libs.versions.toml、gradle.properties、 app/build.gradle.kts、既存の org.tensorflow.lite インポートを検索します。app/build.gradle.kts で minSdk を設定し、LiteRT ランタイムと GPU アクセラレーターの依存関係を追加します。

android {
    defaultConfig {
        minSdk = 24
    }
}

dependencies {
    implementation("com.google.ai.edge.litert:litert:2.3.0")
    implementation("com.google.ai.edge.litert:litert-gpu:2.3.0")
}

バンドルするモデルは app/src/main/assets/model.tflite に配置します(AGP はデフォルトで .tflite ファイルを非圧縮のまま保存します)。ベース APK のサイズ上限を超えるモデルは、 AI パックとして Play for On-Device AI を通じて配信するか、 context.filesDir にダウンロードします(bundletool --local-testing を使うと、Google Play を経由せずに AI パックをローカルにインストールできます)。インストール時に提供される AI パックは AssetManager で読み込みます。ファストフォローまたはオンデマンドの AI パック、あるいは ダウンロードしたファイルは、ステップ 2 のアセット呼び出しの代わりに、ディスク上の filePath から CompiledModel.create(filePath, CompiledModel.Options(accelerator), environment) を使って読み込みます。AI Delivery ライブラリ (com.google.android.play:ai-delivery)の AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath() でパックのディレクトリを 取得します。このメソッドは、パックのダウンロードが完了するまで null を返します。

2.3.0 では、gradle.properties に追加する項目はありません。litert 2.3.0 は 名前空間 com.google.ai.edge.litert.impl を宣言し、その依存関係である litert-api 2.3.0 は com.google.ai.edge.litert を宣言するため、AGP 9.x でプロジェクトをビルドできます。 2.2.0 または 2.1.6 では、両方のライブラリが com.google.ai.edge.litert を宣言するため、 AGP 9.x は processDebugMainManifest で処理を停止します(AGP 8.x では警告が表示されますが、 ビルドは成功します)。これら 2 つのバージョンでは gradle.properties に android.uniquePackageNames=false を追加してください。2.3.0 に移行したら、この設定を削除してください。 この設定は、他の 2 つのライブラリ間で同じ名前空間の競合が発生した場合も隠してしまうためです。 代わりに litert-api を除外する方法は機能しません。CompiledModel と Accelerator は litert-api に含まれているため、ビルドは compileDebugKotlin で失敗します。

ステップ 2:推論ヘルパークラスを実装する

CompiledModel とその TensorBuffer インスタンスを、Android UI 型に依存しない専用の AutoCloseable ヘルパークラスにまとめます。

import android.content.Context
import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
import com.google.ai.edge.litert.Environment
import com.google.ai.edge.litert.TensorBuffer

const val INPUT_SIZE = 224 * 224 * 3
private val environment by lazy { Environment.create() }

class Classifier(context: Context, accelerator: Accelerator) : AutoCloseable {
    private val model = CompiledModel.create(
        context.assets,
        "model.tflite",
        CompiledModel.Options(accelerator),
        environment
    )
    private val inputs = mutableListOf<TensorBuffer>()
    private val outputs = mutableListOf<TensorBuffer>()

    init {
        try {
            inputs += model.createInputBuffers()
            outputs += model.createOutputBuffers()
            infer(FloatArray(INPUT_SIZE))
        } catch (e: Exception) {
            close()
            throw e
        }
    }

    fun infer(input: FloatArray): FloatArray {
        inputs[0].writeFloat(input)
        model.run(inputs, outputs)
        return outputs[0].readFloat()
    }

    override fun close() {
        (inputs + outputs).forEach { it.close() }
        model.close()
    }
}

Accelerator.CPU はデバイス上で何も必要としません。Accelerator.GPU は GPU 向けにグラフをコンパイルします。GPU がモデルを実行できない場合、コンストラクターは LiteRtException をスローします(GPU がサポートしていない演算子の場合は create から、Android エミュレーターでは create またはバッファーの割り当てからスローされます)。アセットが見つからない場合や、有効なモデルファイルでない場合も、create から LiteRtException がスローされます。

バッファーはモデルとともに一度だけ作成され、推論ごとに再利用され、モデルより先にクローズされます。プロセス内のすべてのモデルで 1 つの Environment を使い、オープンしたままにすることで、LiteRT は create の呼び出しごとではなく、GPU アクセラレーターライブラリーを一度だけ読み込みます。コンストラクターの最後にウォームアップ推論を 1 回実行します(最初の GPU 実行にはシェーダーのコンパイルが含まれます)。init のいずれかの処理で例外がスローされた場合は、モデルとそのバッファーをクローズします。INPUT_SIZE はモデルの入力要素数です。writeFloat は長い配列を渡すと例外をスローし、短い配列はエラーなしで書き込むため、サイズが間違っていると正しくない結果になります。int8 または uint8 テンソルの場合は、TensorBuffer の writeFloat と readFloat の代わりに writeInt8 と readInt8 を使用してください。

ステップ 3:ViewModel と Compose 画面を接続する

AndroidViewModel 内の単一スレッドエグゼキューターからヘルパーを実行し、GPU のコンパイルに失敗した場合は Accelerator.CPU にフォールバックして、Compose UI に不変の状態を公開します。

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.LiteRtException
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class UiState(
    val ready: Boolean = false,
    val accelerator: Accelerator = Accelerator.CPU,
    val result: List<Float>? = null,
    val error: String? = null
)
class MainViewModel(app: Application) : AndroidViewModel(app) {
    private val executor = Executors.newSingleThreadExecutor()
    private val scope = CoroutineScope(
        SupervisorJob() + executor.asCoroutineDispatcher()
    )
    private var classifier: Classifier? = null
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun load(preferred: Accelerator = Accelerator.GPU) {
        scope.launch {
            if (classifier != null) return@launch
            val appContext = getApplication<Application>()
            val selected = try {
                classifier = Classifier(appContext, preferred)
                preferred
            } catch (e: LiteRtException) {
                if (preferred != Accelerator.CPU) {
                    try {
                        classifier = Classifier(appContext, Accelerator.CPU)
                        Accelerator.CPU
                    } catch (cpuError: LiteRtException) {
                        _uiState.value = UiState(error = cpuError.message)
                        return@launch
                    }
                } else {
                    _uiState.value = UiState(error = e.message)
                    return@launch
                }
            }
            _uiState.value = UiState(ready = true, accelerator = selected)
        }
    }

    fun classify(input: FloatArray) {
        scope.launch {
            try {
                classifier?.infer(input)?.let { result ->
                    _uiState.update {
                        it.copy(result = result.toList(), error = null)
                    }
                }
            } catch (e: LiteRtException) {
                _uiState.update { it.copy(error = e.message) }
            }
        }
    }

    override fun onCleared() {
        scope.launch { classifier?.close() }.invokeOnCompletion {
            scope.cancel()
        }
        executor.shutdown()
    }
}

単一スレッドのエグゼキューターがモデルを所有します。作成、推論、クローズはこのエグゼキューター上でのみ実行され、メインスレッドでは実行されません。ViewModel は独自のスコープを保持します。viewModelScope は onCleared の実行前にキャンセルされるため、そこで起動したクローズ処理は実行されないからです。onCleared は close の完了後に scope をキャンセルします。 設定変更をまたいでモデルがすでに読み込まれている場合、load は何も実行しません。load が失敗した場合、classifier は null のままです。classify は(パッケージ com.google.ai.edge.litert の)LiteRtException をすべて error に報告し、モデルは保持します。

以下の Compose 画面は、フォトピッカーまたは CameraX から画像(bitmap)を受け取り、クリックハンドラー内で preprocess を実行します(224x224 の場合は数ミリ秒です。224x224 を超える画像では、preprocess をエグゼキューター上で実行してください)。viewModel と collectAsStateWithLifecycle は、androidx.lifecycle:lifecycle-viewmodel-compose および androidx.lifecycle:lifecycle-runtime-compose(2.10.0)から利用できます。

import android.graphics.Bitmap
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.ai.edge.litert.Accelerator
@Composable
fun MainScreen(
    bitmap: Bitmap,
    viewModel: MainViewModel = viewModel()
) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    LaunchedEffect(Unit) { viewModel.load(Accelerator.GPU) }
    Column {
        state.error?.let { Text(it) }
        Button(
            onClick = { viewModel.classify(preprocess(bitmap)) },
            enabled = state.ready
        ) {
            Text("Run (${state.accelerator})")
        }
        state.result?.let { scores ->
            Text("Top class: ${scores.indices.maxBy { i -> scores[i] }}")
        }
    }
}
ステップ 4:モデルの仕様に合わせて入力を前処理する

Bitmap を preprocess でモデルの入力に変換します。モデルの正確な入力サイズ、正規化の平均値と標準偏差、チャンネルの順序、メモリレイアウトを一致させてください(この例では NHWC、RGB、-1.0..1.0 にスケーリングしています)。mean または std の値が一致していないと、モデルが壊れているように見える不正な予測結果になります。

import android.graphics.Bitmap

fun preprocess(
    bitmap: Bitmap,
    size: Int = 224,
    mean: Float = 127.5f,
    std: Float = 127.5f
): FloatArray {
    val scaled = checkNotNull(
        Bitmap.createScaledBitmap(bitmap, size, size, true)
            .copy(Bitmap.Config.ARGB_8888, false)
    )
    val pixels = IntArray(size * size).also {
        scaled.getPixels(it, 0, size, 0, 0, size, size)
    }
    val out = FloatArray(size * size * 3)
    pixels.forEachIndexed { i, p ->
        out[i * 3] = ((p shr 16 and 0xFF) - mean) / std
        out[i * 3 + 1] = ((p shr 8 and 0xFF) - mean) / std
        out[i * 3 + 2] = ((p and 0xFF) - mean) / std
    }
    return out
}

Android 9 以降では ImageDecoder がデフォルトで Bitmap.Config.HARDWARE のビットマップを返し(フォトピッカーの Uri から取得した場合も含みます)、スケーリングしてもこの設定は保持され、getPixels は HARDWARE ビットマップを読み取れないため、スケーリングしたビットマップは Bitmap.Config.ARGB_8888 にコピーしています。コピー対象は画像全体ではなく、224x224 にスケーリングしたビットマップです。スケーリングではアスペクト比が維持されません。モデルで中央クロップが必要な場合は、先にクロップしてください。音声モデルや埋め込みモデルでは、preprocess を独自の入力エンコード処理に置き換え、推論クラスはそのまま使用します。

ステップ 5:物理デバイスで出力を検証する

Android を実行する物理デバイスで、Android Studio からアプリを実行します。まず Accelerator.CPU、次に Accelerator.GPU で、1 つの固定入力に対するアプリの出力をモデルの参照出力と比較します。物理デバイスでモデルの出力を検証する際は、デバイス検証に従う必要があります。

旧版 TFLite Interpreter からの移行

org.tensorflow.lite.Interpreter(org.tensorflow:tensorflow-lite* または com.google.ai.edge.litert:litert 1.x 由来)を使用している既存アプリは、以下の順序で 7 つの手順を実行し、手順 1〜5 の CompiledModel コードに移行します。既存アプリのファイルを変更する前に、各手順のコマンドとコードが記載された移行ガイドを必ず読んでください。

  1. ランタイムを特定する:ビルドファイルと解決済みの依存関係を確認し、アプリが使用するデリゲート、スレッド数、Support ライブラリのクラスを記録します。
  2. 最初に正解出力を取得する:レガシーランタイムがまだプロジェクト内にある状態で、固定入力 1 つに対するアプリの CPU 出力を記録します。この手順にはデバイスまたはエミュレーターが必要です。キャプチャテストは connectedDebugAndroidTest ではなく am instrument で実行してください。connectedDebugAndroidTest はアプリをアンインストールし、ファイルを削除するためです。
  3. 依存関係を入れ替える:レガシーアーティファクトをすべて削除し、手順 1 の依存関係を追加します。Support ライブラリを使用しているアプリでは、com.google.ai.edge.litert:litert-support:2.3.0 を引き続き使用し、パッケージ名を org.tensorflow.lite.support から com.google.ai.edge.litert.support に変更します(LiteRT Support 移行ガイド)。
  4. デリゲートを置き換える:GpuDelegate は CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) に置き換えます。これにより、GPU がサポートしていない演算子は CPU で実行されます。アプリで NNAPI を選択していた場合は、同じ GPU オプションに対応します。Accelerator.CPU のみ、または NPU アクセラレーターには対応しません。setNumThreads(n) は、cpuOptions = CompiledModel.CpuOptions(numThreads = n) を指定した新しい CPU オプションに置き換えます。
  5. 推論コードを書き換える:Interpreter と FileUtil.loadMappedFile を、手順 2 の Classifier ヘルパーに置き換えます。これは手順 3 の単一スレッドエグゼキューター上でのみ作成、実行し、メインスレッドから呼び出されるコンストラクター内では決して実行しないでください。
  6. 2 段階で確認する:ビルドが成功し、レガシー参照を検索して何も出力されないことを確認します。その後、デバイス上で移行後の出力が正解出力と一致することを確認します。
  7. 変更点と移行されなかった機能を報告する:アプリが NNAPI を使用していた場合は、現在は CPU を第 2 のアクセラレーターとして GPU で実行されることをユーザーに伝えます。これにより、以前 DSP または NPU で実行していた量子化モデルの速度と数値結果が変わります。NPU についてはLiteRT による NPU アクセラレーションを案内してください。このスキルでは移行されない LiteRT in Google Play services、TensorFlow Lite Task Library、C++ モジュール、Select TensorFlow operators についてもユーザーに伝えてください。

すべての移行対象アプリに適用される移行要件:

  • org.tensorflow.lite のインポートは禁止です:移行後のコードでは、org.tensorflow.lite.DataType を含め、org.tensorflow.lite のクラスをインポートしてはなりません。LiteRT アーティファクトでは、後方互換性のためにこれらのクラスが残されています。DataType が必要な Support の呼び出し(TensorImage(DataType.FLOAT32)、TensorBuffer.createFixedSize、TensorLabel)は、手順 4 の preprocess、CompiledModel のバッファー、および出力スコアと組み合わせたラベルに置き換えてください。追加したものだけでなく、既存の org.tensorflow.lite インポート行をすべて削除してください。

アンチパターン

  • 決して Android のメインスレッドで CompiledModel を作成、実行、またはクローズしないでください。
  • 決して 推論を呼び出すたびに新しい TensorBuffer インスタンスを割り当てないでください。フレームごとの TensorBuffer オブジェクトをクローズしないと、C++ ヒープメモリがリークします。
  • 決して 出力バッファーの読み戻し(outputs[0].readFloat)を含めずに model.run のベンチマークを行わないでください。
  • 決して org.tensorflow:tensorflow-lite* アーティファクト(org.tensorflow:tensorflow-lite-support など)と com.google.ai.edge.litert:* アーティファクトを混在させないでください。どちらも org.tensorflow.lite.* クラスを含んでおり、クラスの重複によるビルドエラーが発生します。
  • 決して com.google.ai.edge.litert:litert:2.3.0 に加えて com.google.ai.edge.litert:litert-gpu:2.3.0 依存関係を追加せずに、LiteRT 2.3.0 で Accelerator.GPU を使用しないでください。2 つ目のアクセラレーターとして CPU を指定すると、モデルは CPU 上で実行されますが、警告は表示されません。

チェックリスト

  • [ ] minSdk が 24 以上に設定され、app/build.gradle.kts に com.google.ai.edge.litert:litert:2.3.0 と com.google.ai.edge.litert:litert-gpu:2.3.0 の両方が追加されていますか?
  • [ ] すべての旧式の org.tensorflow:tensorflow-lite* 依存関係と org.tensorflow.lite.* インポートが削除されていますか?
  • [ ] CompiledModel.create、model.run、close がすべて同じ単一スレッドのバックグラウンドディスパッチャーで実行されていますか?
  • [ ] TensorBuffer インスタンスは一度だけ割り当てられて呼び出し間で再利用され、init またはウォームアップが例外をスローした場合も含め、model.close の前にクローズされていますか?
  • [ ] load でウォームアップ推論が 1 回実行され、Accelerator.GPU が LiteRtException をスローした場合は Accelerator.CPU にフォールバックしますか?
  • [ ] ./gradlew assembleDebug でプロジェクトをコンパイルできますか?

コメント