关于 litert-runtime
面向 Kotlin Android 开发者的编码技能,指导使用 LiteRT CompiledModel 在 CPU 或 GPU 上运行 `.tflite` 模型,并强调线程、缓冲区管理及旧 API 迁移。
主要功能
- 01配置 LiteRT CompiledModel
- 02CPU 与 GPU 推理指导
- 03单线程管理模型生命周期
- 04复用张量缓冲区
- 05迁移旧版 Interpreter API
适用场景
- 01为 Android 应用添加设备端 `.tflite` 推理
- 02将应用从旧版 Interpreter API 迁移
- 03处理 GPU 回退和推理计时
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ android/ skills --skill litert-runtime安装后在 Claude Code 中输入/litert-runtime确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/migration.mdverify.mdreferences/android/google/play/on-device-ai.md详细介绍
这项技能将分步骤指导你构建 Android 应用,使用 LiteRT CompiledModel API
(com.google.ai.edge.litert:litert 2.x;源代码仓库)在 CPU 或
GPU 上运行 .tflite 模型。不要使用已弃用的 org.tensorflow.lite.Interpreter API。对于使用 .litertlm 文件的开放文本语言模型,请推荐 litert-lm
技能。
前提条件
- Kotlin Android 项目(
minSdk必须为24或更高,如 LiteRT 2.3.0 AAR 中所声明)。 - 应用级
build.gradle.kts文件中来自 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 加速器由第二个构件提供:仅使用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)。无需添加清单条目:litert所依赖的litert-api会在自身清单中声明 OpenCL 驱动程序库(uses-native-library)。 - 已验证的
.tflite模型及其输入约定(输入维度、均值、标准差、通道顺序和张量布局)。对于有已验证配方的模型,请查看 LiteRT 模型配方。
关键要求
- 单线程约束:必须由单线程执行器或受约束的协程调度器独占
CompiledModel实例。创建、推理和关闭操作必须在同一个后台线程上执行,绝不能在主线程上执行。 - 缓冲区生命周期:使用
createInputBuffers和createOutputBuffers各分配一次TensorBuffer实例,在多次推理中重复使用,并在关闭CompiledModel之前关闭所有输入和输出缓冲区。 - 同步计时:
model.run会将任务加入 GPU 队列,并在 GPU 执行完成前返回。计时务必同时涵盖model.run和输出缓冲区读取(outputs[0].readFloat)。 - 预热与异常安全:在辅助类构造函数的
init代码块中执行一次预热推理,避免首次 GPU 着色器编译阻塞用户的首次交互;并在catch中调用close后重新抛出异常,以免缓冲区分配或预热抛出异常时泄漏CompiledModel或TensorBuffer句柄。 - 迁移:如果应用已使用
org.tensorflow.lite代码,在编辑任何文件前,必须完整阅读“从旧版 TFLite Interpreter 迁移”部分。迁移后的代码不得导入任何org.tensorflow.lite类(包括DataType);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 大小限制,请通过 Play 端侧 AI 作为 AI pack 提供模型,或将其下载到
context.filesDir(bundletool --local-testing 可在本地安装 AI pack,无需
Google Play)。通过 AssetManager 读取安装时 AI pack。对于快速跟进或按需 AI pack,或者已下载的文件,请使用其磁盘上的
filePath 调用 CompiledModel.create(filePath, CompiledModel.Options(accelerator), environment),以此替代第 2 步中的资源调用。使用 AI Delivery 库
(com.google.android.play:ai-delivery)通过
AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()
获取 pack 目录;pack 完成下载前,该方法会返回 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 会发出警告后继续
构建);对于这两个版本,请在 gradle.properties 中添加
android.uniquePackageNames=false。升级到 2.3.0 时,请移除此设置,因为它也会掩盖其他任意两个库之间的同类冲突。改为排除
litert-api 也行不通:CompiledModel 和 Accelerator 位于 litert-api 中,
排除后构建会在 compileDebugKotlin 阶段失败。
步骤 2:实现推理辅助类
将 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(对于 GPU 不支持的算子,异常来自 create;在 Android 模拟器上,异常可能来自 create 或缓冲区分配)。缺少资源文件或文件不是有效模型时,create 也会抛出 LiteRtException。
缓冲区会随模型创建一次,并在每次推理时重复使用;关闭时先关闭缓冲区,再关闭模型。进程中的所有模型共用一个 Environment,且该环境会保持打开状态,因此 LiteRT 只会加载一次 GPU 加速器库,而不是在每次调用 create 时都加载。构造函数最后会执行一次预热推理(首次 GPU 运行会包含着色器编译);如果 init 中的任何步骤抛出异常,则会关闭模型及其缓冲区。INPUT_SIZE 是模型的输入元素数量:writeFloat 遇到长度更大的数组时会抛出异常,而长度更短时不会报错,因此大小错误会导致结果不正确。对于 int8 或 uint8 张量,请改用 TensorBuffer 上的 writeInt8 和 readInt8,而不是 writeFloat 和 readFloat。
步骤 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 会将任何 LiteRtException(位于 com.google.ai.edge.litert 包中)报告到 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 步:预处理输入,使其符合模型约定
使用 preprocess 将 Bitmap 转换为模型输入。确保与模型的确切输入维度、归一化均值和标准差、通道顺序及内存布局一致(此示例使用 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.Config.ARGB_8888,是因为 Android 9 及更高版本默认会让 ImageDecoder 返回 Bitmap.Config.HARDWARE 位图(包括从照片选择器 Uri 返回的位图);缩放会保留该配置,而 getPixels 无法读取 HARDWARE 位图。复制的是缩放后的 224x224 位图,而不是完整图像。缩放不会保留纵横比;如果模型要求中心裁剪,请先进行裁剪。音频模型或嵌入模型应使用自己的输入编码替换 preprocess,并保留推理类。
第 5 步:在实体设备上验证输出
从 Android Studio 在运行 Android 的实体设备上运行应用。先在 Accelerator.CPU 上,再在 Accelerator.GPU 上,将应用对一个固定输入的输出与模型的参考输出进行比较;在实体设备上验证模型输出时,必须遵循设备验证。
从旧版 TFLite Interpreter 迁移
现有应用使用 org.tensorflow.lite.Interpreter(来自
org.tensorflow:tensorflow-lite* 或 com.google.ai.edge.litert:litert
1.x)时,需按以下顺序分七步迁移到步骤 1 至 5 中的 CompiledModel 代码。在更改现有应用的任何文件之前,必须先阅读 migration,其中提供了每个步骤所需的命令和代码:
- 查找运行时:阅读构建文件和已解析的依赖项,并记录应用使用的委托、线程数和 Support 库类。
- 先捕获基准输出 :在旧版运行时仍位于项目中时,在 CPU 上记录应用对一个固定输入的输出。此步骤需要设备或模拟器。使用
am instrument运行捕获测试,而不是使用connectedDebugAndroidTest,因为它会卸载应用并删除文件。 - 替换依赖项 :移除所有旧版制品,并添加步骤 1 中的依赖项。使用 Support 库的应用需保留它,依赖项为
com.google.ai.edge.litert:litert-support:2.3.0,并将包名org.tensorflow.lite.support重命名为com.google.ai.edge.litert.support(LiteRT Support 迁移指南)。 - 替换委托 :
GpuDelegate替换为CompiledModel.Options(Accelerator.GPU, Accelerator.CPU),这样 GPU 不支持的运算符就会由 CPU 运行。应用中的 NNAPI 选项对应相同的 GPU 选项,而不是单独使用Accelerator.CPU或使用 NPU 加速器。setNumThreads(n)替换为新的 CPU 选项:cpuOptions = CompiledModel.CpuOptions(numThreads = n)。 - 重写推理代码 :将
Interpreter和FileUtil.loadMappedFile替换为步骤 2 中的Classifier辅助类,并且仅在步骤 3 中的单线程执行器上创建和运行该类;绝不能在主线程调用的构造函数中创建和运行。 - 分两个阶段验证:首先确保构建成功,且搜索不到任何旧版引用;然后在设备上确认迁移后的输出与基准输出一致。
- 说明更改内容和未迁移内容 :如果应用使用了 NNAPI,请告知用户现在应用会使用 GPU 运行,并将 CPU 作为第二加速器;这会改变此前在 DSP 或 NPU 上运行的量化模型的速度和数值结果。请引导用户参阅 使用 LiteRT 进行 NPU 加速,了解 NPU。请告知用户,此技能不会迁移 Google Play 服务中的 LiteRT、TensorFlow Lite Task Library、C++ 模块和 Select TensorFlow 运算符。
适用于所有已迁移应用的迁移要求:
- 不得导入
org.tensorflow.lite:迁移后的代码不得导入任何org.tensorflow.lite类,包括org.tensorflow.lite.DataType;LiteRT 制品保留这些类仅用于向后兼容。对于需要DataType的 Support 调用(TensorImage(DataType.FLOAT32)、TensorBuffer.createFixedSize和TensorLabel),请改用步骤 4 中的preprocess、CompiledModel缓冲区,以及与输出分数配对的标签。删除所有现有的org.tensorflow.lite导入行,不仅是新添加的导入行。
反模式
- 绝不在 Android 主线程上创建、运行或关闭
CompiledModel。 - 绝不在每次推理调用时分配新的
TensorBuffer实例;未关闭每帧的TensorBuffer对象会导致 C++ 堆内存泄漏。 - 绝不在基准测试
model.run时遗漏输出缓冲区回读(outputs[0].readFloat)。 - 绝不混用
org.tensorflow:tensorflow-lite*工件(例如org.tensorflow:tensorflow-lite-support)和com.google.ai.edge.litert:*工件;两者都会提供org.tensorflow.lite.*类,并导致重复类构建失败。 - 绝不在 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 作为第二个加速器,模型会悄无声息地在 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实例是否仅分配一次、在各次调用间重复使用,并在model.close之前关闭(包括init或预热抛出异常的情况)? - [ ]
load是否执行一次预热推理,并在Accelerator.GPU抛出LiteRtException时回退到Accelerator.CPU? - [ ] 项目是否能通过
./gradlew assembleDebug编译?