litert-runtime
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThis skill provides step-by-step guidance for building an Android app that runs
a model with the LiteRT CompiledModel API
( 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 API. For
open text language models using files, suggest the
skill.
.tflitecom.google.ai.edge.litert:litertorg.tensorflow.lite.Interpreter.litertlmlitert-lm本 skill 提供分步指导,帮助构建一个在 CPU 或 GPU 上运行 模型的 Android 应用,方式是使用 LiteRT CompiledModel API( 2.x;source repository),运行时可以打包在 APK 中,也可以来自 Google Play services。不要使用已弃用的 API。对于使用 文件的开放式文本语言模型,请改用 skill。
.tflitecom.google.ai.edge.litert:litertorg.tensorflow.lite.Interpreter.litertlmlitert-lmPrerequisites
前置条件
- A Kotlin Android project (must be
minSdkor higher, as declared by the LiteRT 2.3.0 AARs).24 - The runtime that Step 1 picks: the Google Play services runtime (, the default; see LiteRT in Google Play services), or the bundled runtime (
com.google.android.gms:play-services-litertandimplementation("com.google.ai.edge.litert:litert:2.3.0")from Google Maven). For the bundled runtime, from 2.3.0 the GPU accelerator is the second artifact: withimplementation("com.google.ai.edge.litert:litert-gpu:2.3.0")alone the app builds, andlitertwithCompiledModel.createthrowsCompiledModel.Options(Accelerator.GPU)because the APK has no GPU accelerator library. WithLiteRtExceptionit doesn't throw: the model runs on the CPU while the app reports the GPU, and only Logcat shows it (CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)). No manifest entry is needed:GPU accelerator could not be loaded and registered, whichlitert-apidepends on, declares the OpenCL driver libraries (litert) in its own manifest.uses-native-library - A verified 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.
.tflite
- 一个 Kotlin Android 项目(必须为
minSdk或更高,正如 LiteRT 2.3.0 AAR 所声明)。24 - Step 1 选择的运行时:Google Play services 运行时(,默认选项;参见 LiteRT in Google Play services),或内置运行时(来自 Google Maven 的
com.google.android.gms:play-services-litert和implementation("com.google.ai.edge.litert:litert:2.3.0"))。对于内置运行时,从 2.3.0 开始,GPU 加速器是第二个 artifact:仅使用implementation("com.google.ai.edge.litert:litert-gpu:2.3.0")时应用可以构建,而litert搭配CompiledModel.create会抛出CompiledModel.Options(Accelerator.GPU),因为 APK 中没有 GPU 加速器库。使用LiteRtException时不会抛出该异常:模型在 CPU 上运行,但应用仍报告 GPU,只有 Logcat 会显示CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)。无需添加 manifest 条目:GPU accelerator could not be loaded and registered依赖的litert会在其 manifest 中声明 OpenCL driver library(litert-api)。uses-native-library - 一个经过验证的 模型及其输入约定(输入维度、mean、standard deviation、channel order 和 tensor layout)。对于已有验证配方的模型,请查看 LiteRT model recipes。
.tflite
Critical mandates
强制要求
- Single-thread confinement : One single-thread executor or confined coroutine dispatcher must own the instance. Creation, inference, and closing must execute on that same background thread, never on the main thread.
CompiledModel - Buffer lifecycle : Allocate instances once using
TensorBufferandcreateInputBuffers, reuse them across inferences, and close all input and output buffers before closing thecreateOutputBuffers.CompiledModel - Synchronous timing : enqueues work on the GPU and returns before GPU execution completes. Always measure
model.runand output buffer readback (model.run) together.outputs[0].readFloat - Warm-up and exception safety : Execute one warm-up inference inside the helper constructor's block so first-run GPU shader compilation doesn't block the first user interaction, and call
initinclosebefore rethrowing so neithercatchnorCompiledModelhandles leak if buffer allocation or warm-up throws.TensorBuffer - 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 ), or also targets devices without Google Play services. Never put both runtimes in the same build variant.
MetadataExtractor - Migration : When the app already uses 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.liteclass (includingorg.tensorflow.lite), every legacy GPU choice (DataType, or the Support library'sGpuDelegate) becomesModel.Device.GPU, and an NNAPI choice maps to that same GPU option.CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
- 单线程约束:必须由一个单线程 executor 或受限 coroutine dispatcher 独占 实例。创建、推理和关闭操作必须在同一个后台线程上执行,绝不能放在主线程上。
CompiledModel - Buffer 生命周期:使用 和
createInputBuffers一次性分配createOutputBuffers实例,在多次推理之间复用,并在关闭TensorBuffer之前关闭所有输入和输出 buffer。CompiledModel - 同步计时:会将 GPU 工作加入队列,并在 GPU 执行完成前返回。必须将
model.run和输出 buffer 回读(model.run)作为一个整体进行测量。outputs[0].readFloat - 预热与异常安全:在辅助类构造函数的 块中执行一次预热推理,避免首次运行的 GPU shader 编译阻塞用户的首次交互;如果 buffer 分配或预热抛出异常,应先调用
init再重新抛出,防止close或CompiledModel句柄泄漏。TensorBuffer - 运行时选择:在 Step 1 之前,必须阅读 LiteRT in Google Play services 并选择运行时。除非应用需要 NPU、自定义 operator、此 skill 无法替换的 Support library class(如 ),或者应用也面向不支持 Google Play services 的设备,否则应使用 Google Play services 运行时。绝不能在同一个 build variant 中同时加入两种运行时。
MetadataExtractor - 迁移:当应用已经使用 代码时,必须完整阅读“从旧版 TFLite Interpreter 迁移”一节,然后才能编辑任何文件。迁移后的代码不得导入任何
org.tensorflow.lite类(包括org.tensorflow.lite);所有旧版 GPU 选项(DataType或 Support library 的GpuDelegate)都应替换为Model.Device.GPU;NNAPI 选项也映射到同一个 GPU 选项。CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)
Detailed steps
详细步骤
Step 1: Configure Gradle and place the model file
Step 1:配置 Gradle 并放置模型文件
Search the project for , ,
, and any existing imports before
editing build files. Then pick the runtime:
libs.versions.tomlgradle.propertiesapp/build.gradle.ktsorg.tensorflow.lite-
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 itsreplaces the
Environmentproperty of Step 2. Use versionenvironmentexactly, and run17.0.0(not./gradlew assembleDebug) after you add it. Switch to the bundled runtime only when that build reports--dry-run, and tell the user, also when the user asked for Google Play services. Never leave the build broken.Could not find com.google.android.gms:play-services-litert:17.0.0 -
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), when the app also targets devices without Google Play services, or when the user asks for it.
MetadataExtractor,FileUtil,ImageProcessor, andTensorImagedon't count: replace them as LiteRT in Google Play services describes. For the bundled runtime, configureTensorLabeland add the LiteRT runtime and GPU accelerator dependencies inminSdk:app/build.gradle.ktsandroid { 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 (AGP stores
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
( installs AI packs locally
without Google Play). Read an install-time AI pack through . Load
a fast-follow or on-demand AI pack or a downloaded file from its on-disk
using in place of the asset call in
Step 2. Obtain the pack's directory from the AI Delivery library
() using
,
which returns until the pack has finished downloading.
app/src/main/assets/model.tflite.tflitecontext.filesDirbundletool --local-testingAssetManagerfilePathCompiledModel.create(filePath, CompiledModel.Options(accelerator), environment)com.google.android.play:ai-deliveryAiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()nullWith 2.3.0, nothing is added to : 2.3.0 declares
the namespace and its dependency
2.3.0 declares , so AGP 9.x builds the project. With
2.2.0 or 2.1.6, both libraries declare , and AGP
9.x stops at (AGP 8.x reports it as a warning and
builds); for those two versions, add to
, and remove it when moving to 2.3.0 because it also hides
the same clash between any other two libraries. Excluding instead
doesn't work: and are in , and the
build then fails at .
gradle.propertieslitertcom.google.ai.edge.litert.impllitert-apicom.google.ai.edge.litertcom.google.ai.edge.litertprocessDebugMainManifestandroid.uniquePackageNames=falsegradle.propertieslitert-apiCompiledModelAcceleratorlitert-apicompileDebugKotlin在编辑构建文件之前,搜索项目中的 、、 以及现有的 import。然后选择运行时:
libs.versions.tomlgradle.propertiesapp/build.gradle.ktsorg.tensorflow.lite-
Google Play services 运行时(默认):除非属于下述内置运行时适用场景,否则使用它;即使请求没有指定运行时,也应使用它。现在必须阅读 LiteRT in Google Play services:其中的依赖项将替换本步骤中的依赖项,其将替换 Step 2 的
Environment属性。必须精确使用版本environment,并在添加依赖后运行17.0.0,而不是./gradlew assembleDebug。仅当构建报告--dry-run时,才切换到内置运行时;即使用户明确要求使用 Google Play services,也要告知用户。绝不能让构建处于损坏状态。Could not find com.google.android.gms:play-services-litert:17.0.0 -
内置运行时:仅当应用需要 NPU、自定义 operator、此 skill 无法替换的 Support library class(如),应用也面向不支持 Google Play services 的设备,或者用户明确要求时,才使用它。
MetadataExtractor、FileUtil、ImageProcessor和TensorImage不属于此类:应按照 LiteRT in Google Play services 所述进行替换。对于内置运行时,请在TensorLabel中配置app/build.gradle.kts并添加 LiteRT runtime 和 GPU accelerator 依赖项:minSdkandroid { 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") }
将内置模型放在 (AGP 默认不压缩存储 文件)。对于超过基础 APK 大小限制的模型,应通过 Play for On-Device AI 以 AI pack 形式交付,或者下载到 ( 可在本地安装 AI pack,无需 Google Play)。通过 读取 install-time AI pack。使用 加载 fast-follow 或 on-demand AI pack,或者加载已下载文件的磁盘 ,以此代替 Step 2 中的 asset 调用。通过 AI Delivery library()获取 pack 目录:
app/src/main/assets/model.tflite.tflitecontext.filesDirbundletool --local-testingAssetManagerCompiledModel.create(filePath, CompiledModel.Options(accelerator), environment)filePathcom.google.android.play:ai-deliveryAiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath()在 pack 下载完成之前,该方法返回 。
null对于 2.3.0,不需要向 添加任何内容: 2.3.0 声明 namespace ,其依赖项 2.3.0 声明 ,因此 AGP 9.x 可以构建项目。对于 2.2.0 或 2.1.6,这两个库都会声明 ,AGP 9.x 会停在 (AGP 8.x 会报告警告并继续构建);对于这两个版本,在 中添加 ,升级到 2.3.0 时将其移除,因为它也会隐藏任意两个库之间的同类 namespace 冲突。排除 不可行: 和 位于 中,构建随后会在 阶段失败。
gradle.propertieslitertcom.google.ai.edge.litert.impllitert-apicom.google.ai.edge.litertcom.google.ai.edge.litertprocessDebugMainManifestgradle.propertiesandroid.uniquePackageNames=falselitert-apiCompiledModelAcceleratorlitert-apicompileDebugKotlinStep 2: Implement the inference helper class
Step 2:实现推理辅助类
Isolate and its instances in a dedicated
helper class free of Android UI types:
CompiledModelTensorBufferAutoCloseableimport 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.CPUAccelerator.GPULiteRtExceptioncreatecreateLiteRtExceptioncreateThe buffers are created once with the model, reused for every inference, and
closed before the model. One serves every model in the process and
stays open, so LiteRT loads the GPU accelerator library once rather than on
every 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 throws. is the model's input element count:
throws on a longer array and writes a shorter one without an error,
so a wrong size produces an incorrect result. For or tensors, use
and on instead of and
.
EnvironmentcreateinitINPUT_SIZEwriteFloatint8uint8writeInt8readInt8TensorBufferwriteFloatreadFloatWhen memory matters, use the following options:
-
Load from a path :keeps a copy of the asset's bytes in the native heap while the model is open. Copy the asset once into
create(context.assets, ...)(a downloaded or AI pack file is already on disk) and callcontext.noBackupFilesDirso 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:create(filePath, ...)import android.content.Context import com.google.ai.edge.litert.Accelerator import com.google.ai.edge.litert.CompiledModel import java.io.Filefun 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 (): 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.
xnnPackWeightCachePath -
Transient indirection buffer () : Default
xnnPackFlags = 0x23plus flag0x3, 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,0x20alone leaves0x20andint8operators to fallback kernels.uint8is ignored on the GPU.cpuOptions -
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.
将 及其 实例隔离在一个专用的 辅助类中,且不要包含 Android UI 类型:
CompiledModelTensorBufferAutoCloseableimport 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.CPUAccelerator.GPULiteRtExceptioncreatecreatecreateLiteRtException这些 buffer 与模型一起创建一次,在每次推理时复用,并在模型之前关闭。进程中的每个模型共用一个 ,且该 Environment 保持打开状态,因此 LiteRT 只加载一次 GPU accelerator library,而不是在每次调用 时加载。构造函数最后会执行一次预热推理,因为首次 GPU 运行包含 shader 编译;如果 中任何步骤抛出异常,则关闭模型及其 buffer。 是模型的输入元素数量:如果传入的数组过长, 会抛出异常;如果过短,则不会报错,但会写入不完整数据,从而产生错误结果。对于 或 tensor,应在 上使用 和 ,而不是 和 。
EnvironmentcreateinitINPUT_SIZEwriteFloatint8uint8TensorBufferwriteInt8readInt8writeFloatreadFloat当内存占用很重要时,可以使用以下选项:
-
从路径加载:会在模型打开期间将 asset 字节副本保留在 native heap 中。将 asset 一次性复制到
create(context.assets, ...)(已下载的文件或 AI pack 文件本来就在磁盘上),然后调用context.noBackupFilesDir,这样文件可以采用内存映射方式并由系统回收。为暂存文件设置版本;内置模型更新时递增版本,并删除上一个版本的文件。先复制到临时文件再重命名,确保进程意外终止导致复制中断时,不会加载不完整文件:create(filePath, ...)import android.content.Context import com.google.ai.edge.litert.Accelerator import com.google.ai.edge.litert.CompiledModel import java.io.Filefun 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():首次加载时写入,其大小约为 FP32 模型文件大小的 ~1 倍或 FP16 模型文件大小的 ~2 倍;后续加载时采用内存映射,从而将打包后的 FP32 权重移出 heap。为 cache 文件名设置版本,并删除过期 cache。
xnnPackWeightCachePath -
Transient indirection buffer():默认值为
xnnPackFlags = 0x23,再加上 flag0x3,以单个共享 buffer 替代各 convolution operator 的指针表(输出逐字节一致,4 个线程时延迟增加约 ~1%,单线程时增加约 ~3%)。由于该值会替换默认值,单独使用0x20会使0x20和int8operator 回退到 fallback kernel。GPU 会忽略uint8。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 , and
fall back to when GPU compilation fails:
AndroidViewModelAccelerator.CPUimport 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 keeps its own
scope because is canceled before runs, so a close
launched there wouldn't run; cancels once completes.
is a no-op when a model is already loaded across configuration changes; a
failed leaves . reports any
(in package ) in and keeps
the model.
ViewModelviewModelScopeonClearedonClearedscopecloseloadloadclassifiernullclassifyLiteRtExceptioncom.google.ai.edge.literterrorThe following Compose screen takes the picture () from the photo picker
or CameraX and runs in the click handler (a few milliseconds for
224x224; move onto the executor for images exceeding 224x224).
and come from
and
:
bitmappreprocesspreprocessviewModelcollectAsStateWithLifecycleandroidx.lifecycle:lifecycle-viewmodel-composeandroidx.lifecycle:lifecycle-runtime-composeimport 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] }}")
}
}
}在 内部使用单线程 executor 驱动辅助类,并在 GPU 编译失败时回退到 :
AndroidViewModelAccelerator.CPUimport 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 上执行,绝不在主线程上执行。 保留自己的 scope,因为 会在 运行之前被取消,因此在其中启动的关闭操作不会执行; 会在 完成后取消 。当模型已加载时, 在配置变更后仍是空操作;加载失败时, 保持为 。 会通过 报告任何 (位于 package 中),并保留模型。
ViewModelviewModelScopeonClearedonClearedclosescopeloadclassifiernullclassifyerrorLiteRtExceptioncom.google.ai.edge.litert以下 Compose 界面从 photo picker 或 CameraX 获取图片(),并在 click handler 中运行 (224x224 通常只需数毫秒;如果图片大于 224x224,应将 移至 executor 上执行)。 和 分别来自 和 :
bitmappreprocesspreprocessviewModelcollectAsStateWithLifecycleandroidx.lifecycle:lifecycle-viewmodel-composeandroidx.lifecycle:lifecycle-runtime-composeimport 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 into the model's input with . Match the
model's exact input dimensions, normalization mean and standard deviation,
channel order, and memory layout (this example uses NHWC, RGB, scaled to
). Mismatched or values produce corrupted predictions
that look like a broken model:
Bitmappreprocess-1.0..1.0meanstdimport 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 because
returns bitmaps by default on Android 9 and higher
(including from a photo picker ), scaling preserves that config, and
can't read 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 with its own input encoding and keeps the inference class.
Bitmap.Config.ARGB_8888ImageDecoderBitmap.Config.HARDWAREUrigetPixelsHARDWAREpreprocess使用 将 转换为模型输入。必须匹配模型准确的输入维度、归一化 mean 和 standard deviation、channel order 以及 memory layout(以下示例使用 NHWC、RGB,并缩放到 )。如果 或 值不匹配,预测结果会损坏,看起来像是模型本身出现了问题:
preprocessBitmap-1.0..1.0meanstdimport 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 复制到 ,因为在 Android 9 及更高版本中, 默认返回 bitmap(photo picker 的 也包含在内),而缩放操作会保留该 config, 无法读取 bitmap。复制的是缩放到 224x224 后的 bitmap,而不是完整图片。缩放不会保留宽高比;如果模型要求 center cropping,应先进行裁剪。音频或 embedding 模型应将 替换为自身的输入编码方式,并继续使用推理类。
Bitmap.Config.ARGB_8888ImageDecoderBitmap.Config.HARDWAREUrigetPixelsHARDWAREpreprocessStep 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
first and then on ; when verifying model
outputs on a physical device, you must follow device verification.
Accelerator.CPUAccelerator.GPU在运行 Android Studio 所支持 Android 版本的物理设备上运行应用。先在 上将应用输出与模型针对一个固定输入的 reference output 进行比较,然后在 上进行比较;在物理设备上验证模型输出时,必须遵循 device verification。
Accelerator.CPUAccelerator.GPUMigrating from legacy TFLite Interpreter
从旧版 TFLite Interpreter 迁移
An existing app on (from
or from
1.x) moves to the 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
() follows LiteRT in Google Play services
instead:
org.tensorflow.lite.Interpreterorg.tensorflow:tensorflow-lite*com.google.ai.edge.litert:litertCompiledModelplay-services-tflite-java- 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.
- 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 , not
am instrument, which uninstalls the app and deletes the file.connectedDebugAndroidTest - 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 . 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
play-services-litert, with the packagecom.google.ai.edge.litert:litert-support:2.3.0renamed toorg.tensorflow.lite.support(LiteRT Support migration guide).com.google.ai.edge.litert.support - Replace the delegates : becomes
GpuDelegate, 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 toCompiledModel.Options(Accelerator.GPU, Accelerator.CPU)alone or to an NPU accelerator.Accelerator.CPUbecomes new CPU options withsetNumThreads(n).cpuOptions = CompiledModel.CpuOptions(numThreads = n) - Rewrite the inference code : Replace and
Interpreterwith theFileUtil.loadMappedFilehelper 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 (Classifier).CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) - 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.
- 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 imports : Migrated code must not import any
org.tensorflow.liteclass, includingorg.tensorflow.lite; the LiteRT artifacts keep these classes only for backward compatibility. Replace Support calls that needorg.tensorflow.lite.DataType(DataType,TensorImage(DataType.FLOAT32), andTensorBuffer.createFixedSize) withTensorLabelof Step 4, thepreprocessbuffers, and labels paired with the output scores. Remove every existingCompiledModelimport line, not only the ones that you add.org.tensorflow.lite
现有应用如果使用 (来自 或 1.x),应按以下七个步骤的顺序迁移到 Step 1 至 Step 5 的 代码。在修改现有应用的任何文件之前,必须阅读 migration,其中给出了每个步骤所需的命令和代码。如果应用在 Google Play services 中使用 Interpreter API(),则应遵循 LiteRT in Google Play services:
org.tensorflow.lite.Interpreterorg.tensorflow:tensorflow-lite*com.google.ai.edge.litert:litertCompiledModelplay-services-tflite-java- 查找运行时:阅读构建文件和解析后的依赖项,记录 delegate、线程数以及应用使用的 Support library class。
- 首先捕获 golden output:在旧版运行时仍保留在项目中时,记录应用在 CPU 上针对一个固定输入的输出。此步骤需要设备或 emulator。运行捕获测试时应使用 ,而不是
am instrument,因为后者会卸载应用并删除文件。connectedDebugAndroidTest - 替换依赖项:删除所有旧版 artifact 行(绝不能只将其注释掉),并添加 Step 1 所选运行时的依赖项:默认使用 LiteRT in Google Play services 中的依赖项;如果 Gradle 无法找到 ,则按相同规则回退到内置运行时。在 Play services 运行时上,应按照 LiteRT in Google Play services 所述替换 Support library class。在内置运行时上,应用可以使用
play-services-litert保留这些 class,并将 packagecom.google.ai.edge.litert:litert-support:2.3.0重命名为org.tensorflow.lite.support(LiteRT Support migration guide)。com.google.ai.edge.litert.support - 替换 delegate:变为
GpuDelegate,从而由 CPU 执行 GPU 不支持的 operator。应用中的 NNAPI 选项同样映射到这个 GPU 选项,而不是仅使用CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)或 NPU accelerator。Accelerator.CPU变为新的 CPU 选项setNumThreads(n)。cpuOptions = CompiledModel.CpuOptions(numThreads = n) - 重写推理代码:将 和
Interpreter替换为 Step 2 的FileUtil.loadMappedFile辅助类;该辅助类只能在 Step 3 的单线程 executor 上创建和运行,绝不能由主线程调用的构造函数创建。如果没有调用方像 Step 3 那样在 CPU 上重试,应在 GPU 选项中将 CPU 作为第二个 accelerator(Classifier)。CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) - 分两阶段验证:首先确保构建通过,并且搜索旧版引用时没有任何输出;然后在设备上确认迁移后的输出与 golden output 一致。
- 报告已更改和未迁移的内容:如果应用使用 NNAPI,应告知用户现在使用 GPU,并将 CPU 作为第二个 accelerator;这会改变此前在 DSP 或 NPU 上运行的量化模型的速度和数值结果;并引导用户参阅 NPU acceleration with LiteRT 以使用 NPU。还应告知用户,TensorFlow Lite Task Library、C++ module 和 Select TensorFlow operator 不在此 skill 的迁移范围内。
适用于所有迁移应用的要求:
- 不得存在 import:迁移后的代码不得导入任何
org.tensorflow.lite类,包括org.tensorflow.lite;LiteRT artifact 仅出于向后兼容而保留这些 class。将需要org.tensorflow.lite.DataType的 Support 调用(DataType、TensorImage(DataType.FLOAT32)和TensorBuffer.createFixedSize)替换为 Step 4 的TensorLabel、preprocessbuffer,以及与输出分数配对的 labels。删除每一条现有的CompiledModelimport,而不只是新添加的 import。org.tensorflow.lite
Antipatterns
反模式
- Never create, run, or close on the Android main thread.
CompiledModel - Never allocate new instances on every inference call; failing to close per-frame
TensorBufferobjects leaks C++ heap memory.TensorBuffer - Never benchmark without including output buffer readback (
model.run).outputs[0].readFloat - Never mix artifacts (such as
org.tensorflow:tensorflow-lite*) withorg.tensorflow:tensorflow-lite-supportartifacts; both ship thecom.google.ai.edge.litert:*classes and cause duplicate class build failures.org.tensorflow.lite.* - Never use on LiteRT 2.3.0 without adding the
Accelerator.GPUdependency alongsidecom.google.ai.edge.litert:litert-gpu:2.3.0; with the CPU as a second accelerator, the model silently runs on the CPU.com.google.ai.edge.litert:litert:2.3.0 - Never set on its own to get the transient indirection buffer; the value replaces the runtime's default
xnnPackFlags = 0x20, so pass0x3.0x23
- 绝不在 Android 主线程上创建、运行或关闭 。
CompiledModel - 绝不在每次推理调用时分配新的 实例;未关闭逐帧创建的
TensorBuffer对象会导致 C++ heap 内存泄漏。TensorBuffer - 绝不在未包含输出 buffer 回读()的情况下对
outputs[0].readFloat进行基准测试。model.run - 绝不将 artifact(例如
org.tensorflow:tensorflow-lite*)与org.tensorflow:tensorflow-lite-supportartifact 混用;两者都包含com.google.ai.edge.litert:*类,会导致 duplicate class 构建失败。org.tensorflow.lite.* - 在 LiteRT 2.3.0 上使用 时,绝不添加
Accelerator.GPU之外未配套添加com.google.ai.edge.litert:litert:2.3.0的配置;将 CPU 作为第二个 accelerator 时,模型会在 CPU 上静默运行。com.google.ai.edge.litert:litert-gpu:2.3.0 - 绝不单独设置 来使用 transient indirection buffer;该值会替换运行时默认值
xnnPackFlags = 0x20,因此应传入0x3。0x23
Checklist
检查清单
- [ ] Is set to
minSdkor higher and are both24andcom.google.ai.edge.litert:litert:2.3.0added incom.google.ai.edge.litert:litert-gpu:2.3.0?app/build.gradle.kts - [ ] Are all legacy dependencies and
org.tensorflow:tensorflow-lite*imports removed?org.tensorflow.lite.* - [ ] Do ,
CompiledModel.create, andmodel.runall execute on the same single-thread background dispatcher?close - [ ] Are instances allocated once, reused across calls, and closed before
TensorBuffer, including whenmodel.closeor warm-up throws?init - [ ] Does execute one warm-up inference and fall back to
loadifAccelerator.CPUthrowsAccelerator.GPU?LiteRtException - [ ] Does the project compile with ?
./gradlew assembleDebug - [ ] If memory matters, is the model created from a path with a versioned weight cache and on the CPU, and closed after a task when the next use isn't imminent?
xnnPackFlags = 0x23
- [ ] 是否设置为
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*import?org.tensorflow.lite.* - [ ] 、
CompiledModel.create和model.run是否都在同一个单线程后台 dispatcher 上执行?close - [ ] 实例是否只分配一次、在多次调用之间复用,并在
TensorBuffer之前关闭,包括model.close或预热抛出异常的情况?init - [ ] 是否执行了一次预热推理,并在
load抛出Accelerator.GPU时回退到LiteRtException?Accelerator.CPU - [ ] 项目是否可以通过 构建?
./gradlew assembleDebug - [ ] 如果内存占用很重要,是否在 CPU 上通过带版本控制的 weight cache 和 从路径创建模型,并在下一次使用不在近期时于任务完成后关闭?
xnnPackFlags = 0x23