litert-runtime

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
This skill provides step-by-step guidance for building an Android app that runs a
.tflite
model with the LiteRT CompiledModel API (
com.google.ai.edge.litert:litert
2.x; source repository) on the CPU or the GPU, with the runtime bundled in the APK or from Google Play services. Don't use the deprecated
org.tensorflow.lite.Interpreter
API. For open text language models using
.litertlm
files, suggest the
litert-lm
skill.
本 skill 提供分步指导,帮助构建一个在 CPU 或 GPU 上运行
.tflite
模型的 Android 应用,方式是使用 LiteRT CompiledModel API(
com.google.ai.edge.litert:litert
2.x;source repository),运行时可以打包在 APK 中,也可以来自 Google Play services。不要使用已弃用的
org.tensorflow.lite.Interpreter
API。对于使用
.litertlm
文件的开放式文本语言模型,请改用
litert-lm
skill。

Prerequisites

前置条件

  • A Kotlin Android project (
    minSdk
    must be
    24
    or higher, as declared by the LiteRT 2.3.0 AARs).
  • The runtime that Step 1 picks: the Google Play services runtime (
    com.google.android.gms:play-services-litert
    , the default; see LiteRT in Google Play services), or the bundled runtime (
    implementation("com.google.ai.edge.litert:litert:2.3.0")
    and
    implementation("com.google.ai.edge.litert:litert-gpu:2.3.0")
    from Google Maven). For the bundled runtime, from 2.3.0 the GPU accelerator is the second artifact: with
    litert
    alone the app builds, and
    CompiledModel.create
    with
    CompiledModel.Options(Accelerator.GPU)
    throws
    LiteRtException
    because the APK has no GPU accelerator library. With
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    it doesn't throw: the model runs on the CPU while the app reports the GPU, and only Logcat shows it (
    GPU accelerator could not be loaded and registered
    ). No manifest entry is needed:
    litert-api
    , which
    litert
    depends on, declares the OpenCL driver libraries (
    uses-native-library
    ) in its own manifest.
  • A verified
    .tflite
    model and its input contract (input dimensions, mean, standard deviation, channel order, and tensor layout). For models with a verified recipe, inspect the LiteRT model recipes.
  • 一个 Kotlin Android 项目(
    minSdk
    必须为
    24
    或更高,正如 LiteRT 2.3.0 AAR 所声明)。
  • Step 1 选择的运行时:Google Play services 运行时(
    com.google.android.gms:play-services-litert
    ,默认选项;参见 LiteRT in Google Play services),或内置运行时(来自 Google Maven 的
    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 加速器是第二个 artifact:仅使用
    litert
    时应用可以构建,而
    CompiledModel.create
    搭配
    CompiledModel.Options(Accelerator.GPU)
    会抛出
    LiteRtException
    ,因为 APK 中没有 GPU 加速器库。使用
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    时不会抛出该异常:模型在 CPU 上运行,但应用仍报告 GPU,只有 Logcat 会显示
    GPU accelerator could not be loaded and registered
    。无需添加 manifest 条目:
    litert
    依赖的
    litert-api
    会在其 manifest 中声明 OpenCL driver library(
    uses-native-library
    )。
  • 一个经过验证的
    .tflite
    模型及其输入约定(输入维度、mean、standard deviation、channel order 和 tensor layout)。对于已有验证配方的模型,请查看 LiteRT model recipes。

Critical mandates

强制要求

  • Single-thread confinement : One single-thread executor or confined coroutine dispatcher must own the
    CompiledModel
    instance. Creation, inference, and closing must execute on that same background thread, never on the main thread.
  • Buffer lifecycle : Allocate
    TensorBuffer
    instances once using
    createInputBuffers
    and
    createOutputBuffers
    , reuse them across inferences, and close all input and output buffers before closing the
    CompiledModel
    .
  • Synchronous timing :
    model.run
    enqueues work on the GPU and returns before GPU execution completes. Always measure
    model.run
    and output buffer readback (
    outputs[0].readFloat
    ) together.
  • Warm-up and exception safety : Execute one warm-up inference inside the helper constructor's
    init
    block so first-run GPU shader compilation doesn't block the first user interaction, and call
    close
    in
    catch
    before rethrowing so neither
    CompiledModel
    nor
    TensorBuffer
    handles leak if buffer allocation or warm-up throws.
  • Runtime choice : Before Step 1, you must read LiteRT in Google Play services and choose the runtime. Use the Google Play services runtime unless the app needs the NPU, custom operators, or a Support library class that this skill can't replace (such as
    MetadataExtractor
    ), or also targets devices without Google Play services. Never put both runtimes in the same build variant.
  • Migration : When the app already uses
    org.tensorflow.lite
    code, you must read the "Migrating from legacy TFLite Interpreter" section in full before you edit any file. Migrated code must not import any
    org.tensorflow.lite
    class (including
    DataType
    ), every legacy GPU choice (
    GpuDelegate
    , or the Support library's
    Model.Device.GPU
    ) becomes
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    , and an NNAPI choice maps to that same GPU option.
  • 单线程约束:必须由一个单线程 executor 或受限 coroutine dispatcher 独占
    CompiledModel
    实例。创建、推理和关闭操作必须在同一个后台线程上执行,绝不能放在主线程上。
  • Buffer 生命周期:使用
    createInputBuffers
    和
    createOutputBuffers
    一次性分配
    TensorBuffer
    实例,在多次推理之间复用,并在关闭
    CompiledModel
    之前关闭所有输入和输出 buffer。
  • 同步计时:
    model.run
    会将 GPU 工作加入队列,并在 GPU 执行完成前返回。必须将
    model.run
    和输出 buffer 回读(
    outputs[0].readFloat
    )作为一个整体进行测量。
  • 预热与异常安全:在辅助类构造函数的
    init
    块中执行一次预热推理,避免首次运行的 GPU shader 编译阻塞用户的首次交互;如果 buffer 分配或预热抛出异常,应先调用
    close
    再重新抛出,防止
    CompiledModel
    或
    TensorBuffer
    句柄泄漏。
  • 运行时选择:在 Step 1 之前,必须阅读 LiteRT in Google Play services 并选择运行时。除非应用需要 NPU、自定义 operator、此 skill 无法替换的 Support library class(如
    MetadataExtractor
    ),或者应用也面向不支持 Google Play services 的设备,否则应使用 Google Play services 运行时。绝不能在同一个 build variant 中同时加入两种运行时。
  • 迁移:当应用已经使用
    org.tensorflow.lite
    代码时,必须完整阅读“从旧版 TFLite Interpreter 迁移”一节,然后才能编辑任何文件。迁移后的代码不得导入任何
    org.tensorflow.lite
    类(包括
    DataType
    );所有旧版 GPU 选项(
    GpuDelegate
    或 Support library 的
    Model.Device.GPU
    )都应替换为
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    ;NNAPI 选项也映射到同一个 GPU 选项。

Detailed steps

详细步骤

Step 1: Configure Gradle and place the model file

Step 1:配置 Gradle 并放置模型文件

Search the project for
libs.versions.toml
,
gradle.properties
,
app/build.gradle.kts
, and any existing
org.tensorflow.lite
imports before editing build files. Then pick the runtime:
  • Google Play services runtime (default) : Use it unless one of the bundled runtime cases applies, also when the request doesn't name a runtime. You must read LiteRT in Google Play services now: its dependencies replace the ones in this step, and its
    Environment
    replaces the
    environment
    property of Step 2. Use version
    17.0.0
    exactly, and run
    ./gradlew assembleDebug
    (not
    --dry-run
    ) after you add it. Switch to the bundled runtime only when that build reports
    Could not find com.google.android.gms:play-services-litert:17.0.0
    , and tell the user, also when the user asked for Google Play services. Never leave the build broken.
  • Bundled runtime : Use it only when the app needs the NPU, custom operators, or a Support library class that this skill can't replace (such as
    MetadataExtractor
    ), when the app also targets devices without Google Play services, or when the user asks for it.
    FileUtil
    ,
    ImageProcessor
    ,
    TensorImage
    , and
    TensorLabel
    don't count: replace them as LiteRT in Google Play services describes. For the bundled runtime, configure
    minSdk
    and add the LiteRT runtime and GPU accelerator dependencies in
    app/build.gradle.kts
    :
    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") }
Place bundled models at
app/src/main/assets/model.tflite
(AGP stores
.tflite
files uncompressed by default). Deliver a model that exceeds the base APK size limit through Play for On-Device AI as an AI pack, or download it into
context.filesDir
(
bundletool --local-testing
installs AI packs locally without Google Play). Read an install-time AI pack through
AssetManager
. Load a fast-follow or on-demand AI pack or a downloaded file from its on-disk
filePath
using
CompiledModel.create(filePath, CompiledModel.Options(accelerator), environment)
in place of the asset call in Step 2. Obtain the pack's directory from the AI Delivery library (
com.google.android.play:ai-delivery
) using
AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()
, which returns
null
until the pack has finished downloading.
With 2.3.0, nothing is added to
gradle.properties
:
litert
2.3.0 declares the namespace
com.google.ai.edge.litert.impl
and its dependency
litert-api
2.3.0 declares
com.google.ai.edge.litert
, so AGP 9.x builds the project. With 2.2.0 or 2.1.6, both libraries declare
com.google.ai.edge.litert
, and AGP 9.x stops at
processDebugMainManifest
(AGP 8.x reports it as a warning and builds); for those two versions, add
android.uniquePackageNames=false
to
gradle.properties
, and remove it when moving to 2.3.0 because it also hides the same clash between any other two libraries. Excluding
litert-api
instead doesn't work:
CompiledModel
and
Accelerator
are in
litert-api
, and the build then fails at
compileDebugKotlin
.
在编辑构建文件之前,搜索项目中的
libs.versions.toml
、
gradle.properties
、
app/build.gradle.kts
以及现有的
org.tensorflow.lite
import。然后选择运行时:
  • Google Play services 运行时(默认):除非属于下述内置运行时适用场景,否则使用它;即使请求没有指定运行时,也应使用它。现在必须阅读 LiteRT in Google Play services:其中的依赖项将替换本步骤中的依赖项,其
    Environment
    将替换 Step 2 的
    environment
    属性。必须精确使用版本
    17.0.0
    ,并在添加依赖后运行
    ./gradlew assembleDebug
    ,而不是
    --dry-run
    。仅当构建报告
    Could not find com.google.android.gms:play-services-litert:17.0.0
    时,才切换到内置运行时;即使用户明确要求使用 Google Play services,也要告知用户。绝不能让构建处于损坏状态。
  • 内置运行时:仅当应用需要 NPU、自定义 operator、此 skill 无法替换的 Support library class(如
    MetadataExtractor
    ),应用也面向不支持 Google Play services 的设备,或者用户明确要求时,才使用它。
    FileUtil
    、
    ImageProcessor
    、
    TensorImage
    和
    TensorLabel
    不属于此类:应按照 LiteRT in Google Play services 所述进行替换。对于内置运行时,请在
    app/build.gradle.kts
    中配置
    minSdk
    并添加 LiteRT runtime 和 GPU accelerator 依赖项:
    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 大小限制的模型,应通过 Play for On-Device AI 以 AI pack 形式交付,或者下载到
context.filesDir
(
bundletool --local-testing
可在本地安装 AI pack,无需 Google Play)。通过
AssetManager
读取 install-time AI pack。使用
CompiledModel.create(filePath, CompiledModel.Options(accelerator), environment)
加载 fast-follow 或 on-demand AI pack,或者加载已下载文件的磁盘
filePath
,以此代替 Step 2 中的 asset 调用。通过 AI Delivery library(
com.google.android.play:ai-delivery
)获取 pack 目录:
AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()
在 pack 下载完成之前,该方法返回
null
。
对于 2.3.0,不需要向
gradle.properties
添加任何内容:
litert
2.3.0 声明 namespace
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 会报告警告并继续构建);对于这两个版本,在
gradle.properties
中添加
android.uniquePackageNames=false
,升级到 2.3.0 时将其移除,因为它也会隐藏任意两个库之间的同类 namespace 冲突。排除
litert-api
不可行:
CompiledModel
和
Accelerator
位于
litert-api
中,构建随后会在
compileDebugKotlin
阶段失败。

Step 2: Implement the inference helper class

Step 2:实现推理辅助类

Isolate
CompiledModel
and its
TensorBuffer
instances in a dedicated
AutoCloseable
helper class free of Android UI types:
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
requires nothing from the device.
Accelerator.GPU
compiles the graph for the GPU; when the GPU can't execute the model, the constructor throws
LiteRtException
(from
create
for an operator the GPU doesn't support, and on the Android emulator from
create
or from buffer allocation). A missing asset or a file that isn't a valid model also throws
LiteRtException
from
create
.
The buffers are created once with the model, reused for every inference, and closed before the model. One
Environment
serves every model in the process and stays open, so LiteRT loads the GPU accelerator library once rather than on every
create
call. The constructor ends with one warm-up inference (the first GPU run includes shader compilation) and closes the model and its buffers if any step in
init
throws.
INPUT_SIZE
is the model's input element count:
writeFloat
throws on a longer array and writes a shorter one without an error, so a wrong size produces an incorrect result. For
int8
or
uint8
tensors, use
writeInt8
and
readInt8
on
TensorBuffer
instead of
writeFloat
and
readFloat
.
When memory matters, use the following options:
  • Load from a path :
    create(context.assets, ...)
    keeps a copy of the asset's bytes in the native heap while the model is open. Copy the asset once into
    context.noBackupFilesDir
    (a downloaded or AI pack file is already on disk) and call
    create(filePath, ...)
    so the file is mapped and reclaimable. Version the staged filename, bump the version when the bundled model changes, and delete the previous version's file. Copy to a temporary file and rename it so a copy interrupted by process death is never loaded:
    import android.content.Context import com.google.ai.edge.litert.Accelerator import com.google.ai.edge.litert.CompiledModel import java.io.File
    fun stageModelAsset( context: Context, assetName: String = "model.tflite", version: String = "v1" ): File = File(context.noBackupFilesDir, "$version-$assetName").also { file -> if (!file.isFile) { context.noBackupFilesDir.listFiles()?.forEach { if (it.name.endsWith("-$assetName")) it.delete() } val tmp = File("${file.path}.tmp") context.assets.open(assetName).use { input -> tmp.outputStream().use { output -> input.copyTo(output) } } check(tmp.renameTo(file)) } }
    fun memoryOptions( context: Context, accelerator: Accelerator = Accelerator.CPU, assetName: String = "model.tflite", version: String = "v1" ): CompiledModel.Options = CompiledModel.Options(accelerator).apply { val prefix = "$assetName-" val cache = File(context.codeCacheDir, "$prefix$version.xnnpack_cache") context.codeCacheDir.listFiles()?.forEach { if (it.name.startsWith(prefix) && it.name.endsWith(".xnnpack_cache") && it != cache) { it.delete() } } cpuOptions = CompiledModel.CpuOptions( numThreads = 4, xnnPackFlags = 0x23, xnnPackWeightCachePath = cache.path ) }
  • Weight cache (
    xnnPackWeightCachePath
    )
    : Written on the first load (~1x an FP32 or ~2x an FP16 model's file size) and mapped on later loads, moving packed FP32 weights off the heap. Version the cache filename and delete outdated caches.
  • Transient indirection buffer (
    xnnPackFlags = 0x23
    )
    : Default
    0x3
    plus flag
    0x20
    , replacing per-operator convolution pointer tables with one shared buffer (byte-identical outputs, ~1% latency overhead at 4 threads, ~3% single-threaded). Because the value replaces the default,
    0x20
    alone leaves
    int8
    and
    uint8
    operators to fallback kernels.
    cpuOptions
    is ignored on the GPU.
  • Close after a task: Closing frees the runtime workspace; recreating with a warm cache takes ~20 ms, whereas the first inference after any load pays a full warm-up run, so close when the next use isn't imminent.
将
CompiledModel
及其
TensorBuffer
实例隔离在一个专用的
AutoCloseable
辅助类中,且不要包含 Android UI 类型:
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
(可能是
create
遇到 GPU 不支持的 operator,也可能是 Android emulator 上的
create
或 buffer 分配失败)。缺少 asset 或文件不是有效模型时,
create
同样会抛出
LiteRtException
。
这些 buffer 与模型一起创建一次,在每次推理时复用,并在模型之前关闭。进程中的每个模型共用一个
Environment
,且该 Environment 保持打开状态,因此 LiteRT 只加载一次 GPU accelerator library,而不是在每次调用
create
时加载。构造函数最后会执行一次预热推理,因为首次 GPU 运行包含 shader 编译;如果
init
中任何步骤抛出异常,则关闭模型及其 buffer。
INPUT_SIZE
是模型的输入元素数量:如果传入的数组过长,
writeFloat
会抛出异常;如果过短,则不会报错,但会写入不完整数据,从而产生错误结果。对于
int8
或
uint8
tensor,应在
TensorBuffer
上使用
writeInt8
和
readInt8
,而不是
writeFloat
和
readFloat
。
当内存占用很重要时,可以使用以下选项:
  • 从路径加载:
    create(context.assets, ...)
    会在模型打开期间将 asset 字节副本保留在 native heap 中。将 asset 一次性复制到
    context.noBackupFilesDir
    (已下载的文件或 AI pack 文件本来就在磁盘上),然后调用
    create(filePath, ...)
    ,这样文件可以采用内存映射方式并由系统回收。为暂存文件设置版本;内置模型更新时递增版本,并删除上一个版本的文件。先复制到临时文件再重命名,确保进程意外终止导致复制中断时,不会加载不完整文件:
    import android.content.Context import com.google.ai.edge.litert.Accelerator import com.google.ai.edge.litert.CompiledModel import java.io.File
    fun stageModelAsset( context: Context, assetName: String = "model.tflite", version: String = "v1" ): File = File(context.noBackupFilesDir, "$version-$assetName").also { file -> if (!file.isFile) { context.noBackupFilesDir.listFiles()?.forEach { if (it.name.endsWith("-$assetName")) it.delete() } val tmp = File("${file.path}.tmp") context.assets.open(assetName).use { input -> tmp.outputStream().use { output -> input.copyTo(output) } } check(tmp.renameTo(file)) } }
    fun memoryOptions( context: Context, accelerator: Accelerator = Accelerator.CPU, assetName: String = "model.tflite", version: String = "v1" ): CompiledModel.Options = CompiledModel.Options(accelerator).apply { val prefix = "$assetName-" val cache = File(context.codeCacheDir, "$prefix$version.xnnpack_cache") context.codeCacheDir.listFiles()?.forEach { if (it.name.startsWith(prefix) && it.name.endsWith(".xnnpack_cache") && it != cache) { it.delete() } } cpuOptions = CompiledModel.CpuOptions( numThreads = 4, xnnPackFlags = 0x23, xnnPackWeightCachePath = cache.path ) }
  • Weight cache(
    xnnPackWeightCachePath
    )
    :首次加载时写入,其大小约为 FP32 模型文件大小的 ~1 倍或 FP16 模型文件大小的 ~2 倍;后续加载时采用内存映射,从而将打包后的 FP32 权重移出 heap。为 cache 文件名设置版本,并删除过期 cache。
  • Transient indirection buffer(
    xnnPackFlags = 0x23
    )
    :默认值为
    0x3
    ,再加上 flag
    0x20
    ,以单个共享 buffer 替代各 convolution operator 的指针表(输出逐字节一致,4 个线程时延迟增加约 ~1%,单线程时增加约 ~3%)。由于该值会替换默认值,单独使用
    0x20
    会使
    int8
    和
    uint8
    operator 回退到 fallback kernel。GPU 会忽略
    cpuOptions
    。
  • 任务完成后关闭:关闭操作可释放 runtime workspace;使用已预热 cache 重新创建约需 ~20 ms,而任何一次加载后的首次推理都要承担完整的预热运行,因此当下一次使用不在近期时应当关闭。

Step 3: Wire the ViewModel and Compose screen

Step 3:连接 ViewModel 和 Compose 界面

Drive the helper from a single-thread executor inside an
AndroidViewModel
, and fall back to
Accelerator.CPU
when GPU compilation fails:
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()
    }
}
One single-thread executor owns the model: creation, inference, and closing execute only on it, never on the main thread. The
ViewModel
keeps its own scope because
viewModelScope
is canceled before
onCleared
runs, so a close launched there wouldn't run;
onCleared
cancels
scope
once
close
completes.
load
is a no-op when a model is already loaded across configuration changes; a failed
load
leaves
classifier
null
.
classify
reports any
LiteRtException
(in package
com.google.ai.edge.litert
) in
error
and keeps the model.
The following Compose screen takes the picture (
bitmap
) from the photo picker or CameraX and runs
preprocess
in the click handler (a few milliseconds for 224x224; move
preprocess
onto the executor for images exceeding 224x224).
viewModel
and
collectAsStateWithLifecycle
come from
androidx.lifecycle:lifecycle-viewmodel-compose
and
androidx.lifecycle:lifecycle-runtime-compose
:
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] }}")
        }
    }
}
在
AndroidViewModel
内部使用单线程 executor 驱动辅助类,并在 GPU 编译失败时回退到
Accelerator.CPU
:
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()
    }
}
一个单线程 executor 独占模型:创建、推理和关闭操作只在该 executor 上执行,绝不在主线程上执行。
ViewModel
保留自己的 scope,因为
viewModelScope
会在
onCleared
运行之前被取消,因此在其中启动的关闭操作不会执行;
onCleared
会在
close
完成后取消
scope
。当模型已加载时,
load
在配置变更后仍是空操作;加载失败时,
classifier
保持为
null
。
classify
会通过
error
报告任何
LiteRtException
(位于
com.google.ai.edge.litert
package 中),并保留模型。
以下 Compose 界面从 photo picker 或 CameraX 获取图片(
bitmap
),并在 click handler 中运行
preprocess
(224x224 通常只需数毫秒;如果图片大于 224x224,应将
preprocess
移至 executor 上执行)。
viewModel
和
collectAsStateWithLifecycle
分别来自
androidx.lifecycle:lifecycle-viewmodel-compose
和
androidx.lifecycle:lifecycle-runtime-compose
:
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] }}")
        }
    }
}

Step 4: Preprocess inputs to match the model contract

Step 4:预处理输入以匹配模型约定

Convert the
Bitmap
into the model's input with
preprocess
. Match the model's exact input dimensions, normalization mean and standard deviation, channel order, and memory layout (this example uses NHWC, RGB, scaled to
-1.0..1.0
). Mismatched
mean
or
std
values produce corrupted predictions that look like a broken model:
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
}
The scaled bitmap is copied to
Bitmap.Config.ARGB_8888
because
ImageDecoder
returns
Bitmap.Config.HARDWARE
bitmaps by default on Android 9 and higher (including from a photo picker
Uri
), scaling preserves that config, and
getPixels
can't read
HARDWARE
bitmaps; the copy is of the 224x224 scaled bitmap rather than the full image. Scaling doesn't preserve the aspect ratio; crop first if the model requires center cropping. An audio or embedding model replaces
preprocess
with its own input encoding and keeps the inference class.
使用
preprocess
将
Bitmap
转换为模型输入。必须匹配模型准确的输入维度、归一化 mean 和 standard deviation、channel order 以及 memory layout(以下示例使用 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
}
将缩放后的 bitmap 复制到
Bitmap.Config.ARGB_8888
,因为在 Android 9 及更高版本中,
ImageDecoder
默认返回
Bitmap.Config.HARDWARE
bitmap(photo picker 的
Uri
也包含在内),而缩放操作会保留该 config,
getPixels
无法读取
HARDWARE
bitmap。复制的是缩放到 224x224 后的 bitmap,而不是完整图片。缩放不会保留宽高比;如果模型要求 center cropping,应先进行裁剪。音频或 embedding 模型应将
preprocess
替换为自身的输入编码方式,并继续使用推理类。

Step 5: Verify output on a physical device

Step 5:在物理设备上验证输出

Run the app on a physical device that runs Android from Android Studio. Compare the app's output with the model's reference output for one fixed input on
Accelerator.CPU
first and then on
Accelerator.GPU
; when verifying model outputs on a physical device, you must follow device verification.
在运行 Android Studio 所支持 Android 版本的物理设备上运行应用。先在
Accelerator.CPU
上将应用输出与模型针对一个固定输入的 reference output 进行比较,然后在
Accelerator.GPU
上进行比较;在物理设备上验证模型输出时,必须遵循 device verification。

Migrating from legacy TFLite Interpreter

从旧版 TFLite Interpreter 迁移

An existing app on
org.tensorflow.lite.Interpreter
(from
org.tensorflow:tensorflow-lite*
or from
com.google.ai.edge.litert:litert
1.x) moves to the
CompiledModel
code of Steps 1 to 5 in seven steps, in this order. Before you change any file of an existing app, you must read migration, which gives the commands and code for each step. An app on the Interpreter API in Google Play services (
play-services-tflite-java
) follows LiteRT in Google Play services instead:
  1. Find the runtime: Read the build files and the resolved dependencies, and note the delegate, the thread count, and the Support library classes that the app uses.
  2. Capture a golden output first : While the legacy runtime is still in the project, record the app's output for one fixed input on the CPU. This step needs a device or an emulator. Run the capture test with
    am instrument
    , not
    connectedDebugAndroidTest
    , which uninstalls the app and deletes the file.
  3. Swap the dependencies : Delete every legacy artifact line (never leave it commented out) and add the dependencies of the runtime that Step 1 picks: by default those of LiteRT in Google Play services, with the same fallback to the bundled runtime when Gradle can't find
    play-services-litert
    . On the Play services runtime, replace the Support library classes as LiteRT in Google Play services describes. On the bundled runtime, an app keeps them with
    com.google.ai.edge.litert:litert-support:2.3.0
    , with the package
    org.tensorflow.lite.support
    renamed to
    com.google.ai.edge.litert.support
    (LiteRT Support migration guide).
  4. Replace the delegates :
    GpuDelegate
    becomes
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    , so the CPU runs the operators that the GPU doesn't support. An NNAPI choice in the app maps to the same GPU option, not to
    Accelerator.CPU
    alone or to an NPU accelerator.
    setNumThreads(n)
    becomes new CPU options with
    cpuOptions = CompiledModel.CpuOptions(numThreads = n)
    .
  5. Rewrite the inference code : Replace
    Interpreter
    and
    FileUtil.loadMappedFile
    with the
    Classifier
    helper of Step 2, created and run only on the single-thread executor of Step 3, never in a constructor that the main thread calls. When no caller retries on the CPU as in Step 3, create the helper's GPU option with the CPU as a second accelerator (
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    ).
  6. Verify in two stages: The build passes and the searches for legacy references print nothing; then the migrated output matches the golden output on a device.
  7. Report what changed and what didn't migrate : When the app used NNAPI, tell the user that it now runs on the GPU with the CPU as a second accelerator, which changes the speed and the numerical results of a quantized model that ran on a DSP or an NPU before; point them to NPU acceleration with LiteRT for the NPU. Tell the user about the TensorFlow Lite Task Library, C++ modules, and Select TensorFlow operators, which this skill doesn't migrate.
Migration mandates that apply to every migrated app:
  • No
    org.tensorflow.lite
    imports
    : Migrated code must not import any
    org.tensorflow.lite
    class, including
    org.tensorflow.lite.DataType
    ; the LiteRT artifacts keep these classes only for backward compatibility. Replace Support calls that need
    DataType
    (
    TensorImage(DataType.FLOAT32)
    ,
    TensorBuffer.createFixedSize
    , and
    TensorLabel
    ) with
    preprocess
    of Step 4, the
    CompiledModel
    buffers, and labels paired with the output scores. Remove every existing
    org.tensorflow.lite
    import line, not only the ones that you add.
现有应用如果使用
org.tensorflow.lite.Interpreter
(来自
org.tensorflow:tensorflow-lite*
或
com.google.ai.edge.litert:litert
1.x),应按以下七个步骤的顺序迁移到 Step 1 至 Step 5 的
CompiledModel
代码。在修改现有应用的任何文件之前,必须阅读 migration,其中给出了每个步骤所需的命令和代码。如果应用在 Google Play services 中使用 Interpreter API(
play-services-tflite-java
),则应遵循 LiteRT in Google Play services:
  1. 查找运行时:阅读构建文件和解析后的依赖项,记录 delegate、线程数以及应用使用的 Support library class。
  2. 首先捕获 golden output:在旧版运行时仍保留在项目中时,记录应用在 CPU 上针对一个固定输入的输出。此步骤需要设备或 emulator。运行捕获测试时应使用
    am instrument
    ,而不是
    connectedDebugAndroidTest
    ,因为后者会卸载应用并删除文件。
  3. 替换依赖项:删除所有旧版 artifact 行(绝不能只将其注释掉),并添加 Step 1 所选运行时的依赖项:默认使用 LiteRT in Google Play services 中的依赖项;如果 Gradle 无法找到
    play-services-litert
    ,则按相同规则回退到内置运行时。在 Play services 运行时上,应按照 LiteRT in Google Play services 所述替换 Support library class。在内置运行时上,应用可以使用
    com.google.ai.edge.litert:litert-support:2.3.0
    保留这些 class,并将 package
    org.tensorflow.lite.support
    重命名为
    com.google.ai.edge.litert.support
    (LiteRT Support migration guide)。
  4. 替换 delegate:
    GpuDelegate
    变为
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    ,从而由 CPU 执行 GPU 不支持的 operator。应用中的 NNAPI 选项同样映射到这个 GPU 选项,而不是仅使用
    Accelerator.CPU
    或 NPU accelerator。
    setNumThreads(n)
    变为新的 CPU 选项
    cpuOptions = CompiledModel.CpuOptions(numThreads = n)
    。
  5. 重写推理代码:将
    Interpreter
    和
    FileUtil.loadMappedFile
    替换为 Step 2 的
    Classifier
    辅助类;该辅助类只能在 Step 3 的单线程 executor 上创建和运行,绝不能由主线程调用的构造函数创建。如果没有调用方像 Step 3 那样在 CPU 上重试,应在 GPU 选项中将 CPU 作为第二个 accelerator(
    CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
    )。
  6. 分两阶段验证:首先确保构建通过,并且搜索旧版引用时没有任何输出;然后在设备上确认迁移后的输出与 golden output 一致。
  7. 报告已更改和未迁移的内容:如果应用使用 NNAPI,应告知用户现在使用 GPU,并将 CPU 作为第二个 accelerator;这会改变此前在 DSP 或 NPU 上运行的量化模型的速度和数值结果;并引导用户参阅 NPU acceleration with LiteRT 以使用 NPU。还应告知用户,TensorFlow Lite Task Library、C++ module 和 Select TensorFlow operator 不在此 skill 的迁移范围内。
适用于所有迁移应用的要求:
  • 不得存在
    org.tensorflow.lite
    import
    :迁移后的代码不得导入任何
    org.tensorflow.lite
    类,包括
    org.tensorflow.lite.DataType
    ;LiteRT artifact 仅出于向后兼容而保留这些 class。将需要
    DataType
    的 Support 调用(
    TensorImage(DataType.FLOAT32)
    、
    TensorBuffer.createFixedSize
    和
    TensorLabel
    )替换为 Step 4 的
    preprocess
    、
    CompiledModel
    buffer,以及与输出分数配对的 labels。删除每一条现有的
    org.tensorflow.lite
    import,而不只是新添加的 import。

Antipatterns

反模式

  • Never create, run, or close
    CompiledModel
    on the Android main thread.
  • Never allocate new
    TensorBuffer
    instances on every inference call; failing to close per-frame
    TensorBuffer
    objects leaks C++ heap memory.
  • Never benchmark
    model.run
    without including output buffer readback (
    outputs[0].readFloat
    ).
  • Never mix
    org.tensorflow:tensorflow-lite*
    artifacts (such as
    org.tensorflow:tensorflow-lite-support
    ) with
    com.google.ai.edge.litert:*
    artifacts; both ship the
    org.tensorflow.lite.*
    classes and cause duplicate class build failures.
  • Never use
    Accelerator.GPU
    on LiteRT 2.3.0 without adding the
    com.google.ai.edge.litert:litert-gpu:2.3.0
    dependency alongside
    com.google.ai.edge.litert:litert:2.3.0
    ; with the CPU as a second accelerator, the model silently runs on the CPU.
  • Never set
    xnnPackFlags = 0x20
    on its own to get the transient indirection buffer; the value replaces the runtime's default
    0x3
    , so pass
    0x23
    .
  • 绝不在 Android 主线程上创建、运行或关闭
    CompiledModel
    。
  • 绝不在每次推理调用时分配新的
    TensorBuffer
    实例;未关闭逐帧创建的
    TensorBuffer
    对象会导致 C++ heap 内存泄漏。
  • 绝不在未包含输出 buffer 回读(
    outputs[0].readFloat
    )的情况下对
    model.run
    进行基准测试。
  • 绝不将
    org.tensorflow:tensorflow-lite*
    artifact(例如
    org.tensorflow:tensorflow-lite-support
    )与
    com.google.ai.edge.litert:*
    artifact 混用;两者都包含
    org.tensorflow.lite.*
    类,会导致 duplicate class 构建失败。
  • 在 LiteRT 2.3.0 上使用
    Accelerator.GPU
    时,绝不添加
    com.google.ai.edge.litert:litert:2.3.0
    之外未配套添加
    com.google.ai.edge.litert:litert-gpu:2.3.0
    的配置;将 CPU 作为第二个 accelerator 时,模型会在 CPU 上静默运行。
  • 绝不单独设置
    xnnPackFlags = 0x20
    来使用 transient indirection buffer;该值会替换运行时默认值
    0x3
    ,因此应传入
    0x23
    。

Checklist

检查清单

  • [ ] Is
    minSdk
    set to
    24
    or higher and are both
    com.google.ai.edge.litert:litert:2.3.0
    and
    com.google.ai.edge.litert:litert-gpu:2.3.0
    added in
    app/build.gradle.kts
    ?
  • [ ] Are all legacy
    org.tensorflow:tensorflow-lite*
    dependencies and
    org.tensorflow.lite.*
    imports removed?
  • [ ] Do
    CompiledModel.create
    ,
    model.run
    , and
    close
    all execute on the same single-thread background dispatcher?
  • [ ] Are
    TensorBuffer
    instances allocated once, reused across calls, and closed before
    model.close
    , including when
    init
    or warm-up throws?
  • [ ] Does
    load
    execute one warm-up inference and fall back to
    Accelerator.CPU
    if
    Accelerator.GPU
    throws
    LiteRtException
    ?
  • [ ] Does the project compile with
    ./gradlew assembleDebug
    ?
  • [ ] If memory matters, is the model created from a path with a versioned weight cache and
    xnnPackFlags = 0x23
    on the CPU, and closed after a task when the next use isn't imminent?
  • [ ]
    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.*
    import?
  • [ ]
    CompiledModel.create
    、
    model.run
    和
    close
    是否都在同一个单线程后台 dispatcher 上执行?
  • [ ]
    TensorBuffer
    实例是否只分配一次、在多次调用之间复用,并在
    model.close
    之前关闭,包括
    init
    或预热抛出异常的情况?
  • [ ]
    load
    是否执行了一次预热推理,并在
    Accelerator.GPU
    抛出
    LiteRtException
    时回退到
    Accelerator.CPU
    ?
  • [ ] 项目是否可以通过
    ./gradlew assembleDebug
    构建?
  • [ ] 如果内存占用很重要,是否在 CPU 上通过带版本控制的 weight cache 和
    xnnPackFlags = 0x23
    从路径创建模型,并在下一次使用不在近期时于任务完成后关闭?