Skill: LiteRT Compiled Model Migration SKILL
Description
This skill guides an AI agent to rapidly migrate an Android application from legacy TensorFlow Lite (TFLite) to the modern LiteRT CompiledModel API (2.x) in Open Source GitHub repositories. It prioritizes a high-speed, 1st-pass "Like for Like" baseline migration with automated self-testing, and encourages advanced performance upgrades including off-main-thread execution with buffer reuse and NPU JIT compilation.
Read first: what the Kotlin CompiledModel API (com.google.ai.edge.litert, 2.3.0; 2.2.0 has the same API) has
CompiledModel.create(context.assets, assetName, CompiledModel.Options(Accelerator.CPU), env)andcreate(filePath, options, env);Optionstakes one or more ofAccelerator.CPU/GPU/NPU(e.g.Options(Accelerator.GPU, Accelerator.CPU));envisEnvironment.create(context)(optional; enables the compiler cache) orEnvironment.create(context, BuiltinNpuAcceleratorProvider(context))for the NPU. Nothing takes a ByteBuffer or a mapped file.- Buffers come from the model (
createInputBuffers()/createOutputBuffers(),List<TensorBuffer>;writeFloat(FloatArray)/writeInt8(ByteArray)/writeInt/writeLong,readFloat()and friends; create them once).run(inputs, outputs)is synchronous on the CPU; on the GPU it can return before the GPU finishes andread*()waits; there is no callback API.CompiledModel,TensorBufferandEnvironmentareAutoCloseable: close buffers, then the model, then anEnvironmentthe app created. CompiledModel.Options()with no accelerator compiles, butcreatethen throwsLiteRtException.Interpreter,GpuDelegate,NnApiDelegateand the Support library'sModel(com.google.ai.edge.litert.support.model.Model, which runs onInterpreterApi) do not appear in migrated code; the rest of LiteRT Support does, undercom.google.ai.edge.litert.support(Step 1), withorg.tensorflow.lite.DataTypeas the type its classes take. ThelitertAAR also containsorg.tensorflow.lite.Interpreter, so leftover Interpreter code compiles (all butsetUseNNAPI, which 2.x does not have) and the Stage 1 import check is what catches it.templates/LiteRtModel.ktcompiles against the 2.3.0 and the 2.2.0 artifacts; start from it.
0. Automatic Discovery & Upfront Planning
Before writing code, the agent MUST inspect the project workspace and present the upfront planning interview to align migration parameters:
A. Automatic Workspace & Toolchain Discovery
The agent must automatically inspect the repository structure:
- Ecosystem & Build Engine:
- Gradle Build System: Detected by
build.gradle,build.gradle.kts, orsettings.gradle. -> Enable Gradle & GitHub PR Workflow.
- Gradle Build System: Detected by
- Language & JNI Toolchain:
- Native C++ / NDK: Detected if
CMakeLists.txt,Android.mk, or*.cppfiles exist. -> Enable C++ / JNI Migration Rules. - Pure Kotlin / Java: Default to JVM / Android SDK Migration Rules.
- Native C++ / NDK: Detected if
B. Upfront User Interview (Questions Asked Prior to Migration)
The agent must present the following review options to the user:
Before initiating the LiteRT Compiled Model Migration, please confirm your project preferences: 1. Model Workload & Domain: What type of data does this application process? - [A] Vision (Images / Video / Camera Feeds) -> Enables the camera-frame recipe: preprocess into a reused input buffer via writeFloat / writeInt8 (AHardwareBuffer interop is C++-only). - [B] Audio (Speech / Sound Classification) -> Enables the streaming FloatArray recipe (writeFloat per window). - [C] Text / NLP / GenAI -> Enables the token-id recipe (writeInt / writeLong into the model's input buffers). 2. Hardware Acceleration & Conditional INT8 Quantization: Do you want to enable NPU hardware acceleration via JIT on-device compilation? - [A] Yes (Recommended - replaces deprecated NNAPI; needs the vendor libraries of Upgrade 2.C on a supported SoC) [Default] * If the app uses a Float32 model: Would you like to generate an INT8 integer-quantized model via AI Edge Quantizer for peak NPU speed, or run the original Float32 model? -> Option A.1: Convert to INT8 (Generates model_int8.tflite with int8 weights and activations; needs a few representative inputs for calibration) [Default] -> Option A.2: Keep Float32 (Runs baseline float model directly on NPU) - [B] No (GPU and CPU acceleration only) 3. Encouraged Performance Upgrades: Should the agent upgrade the calling code to use LiteRT's advanced features? - [A] Yes (Move inference off the main thread and reuse buffers) [Default] - [B] No (Keep strict 1-to-1 synchronous baseline execution) 4. Automated Pull Request Provisioning: Should the agent automatically stage, commit, and create a GitHub PR when self-testing passes? - [A] Yes [Default] (Attaches verification test logs and before/after summary diff) - [B] No (Keep changes local in current working branch)
With no user to answer (CI, batch runs): take Q1 from the code, Q2 [A] only when the vendor libraries of Upgrade 2.C are already in the repo (otherwise [B]), Float32 unless calibration inputs exist, and the defaults of Q3 and Q4 with the PR opened as a draft; record the choices in the PR description.
Today the runtime is the standalone com.google.ai.edge.litert:litert AAR: no LiteRT artifact for Google Play services is on Google Maven as of October 2026 (only the play-services-tflite-* Interpreter artifacts exist).
[!IMPORTANT] LiteRT Support (
litert-support2.3.0): an app on the TensorFlow Lite Support Library keeps it. The library now ships as LiteRT Support, with the same classes, methods and behavior; the migration is two steps of Step 1, swap the Gradle coordinates and rename the packageorg.tensorflow.lite.supporttocom.google.ai.edge.litert.support(its guide: https://github.com/google-ai-edge/LiteRT/blob/main/litert/support_lib/MIGRATION_GUIDE.md).ImageProcessor,ResizeOp,NormalizeOp,TensorImage,TensorLabel,FileUtilandMetadataExtractorstay as they are. The one class replaced is itsModel, which runs on the Interpreter API: it becomesCompiledModellikeInterpreter(Step 4), withtemplates/SupportBridge.ktbetween the Support tensors and the model's buffers. An app that never used the Support library does not add it;Bitmap.toModelInputintemplates/LiteRtModel.ktis its preprocessing.
1. Phase 1: "Like for Like" Baseline Migration (1st Pass Success)
Phase 1 prioritizes functional equivalence, fast compilation, and immediate 1st pass self-test success.
Step 0: Capture the Golden Output
Before Step 1 changes any dependency, with the legacy runtime and preprocessing still in the project (no device attached: skip Step 0, stop after Stage 1, and open the PR as a draft that says so):
- Define one fixed input: for a vision app a deterministic bitmap (
fixedBitmapintemplates/GoldenCaptureTest.kt, or a small image file inapp/src/androidTest/assets/), otherwise a generated array; never a camera frame. A bitmap of the model's input size with rotation 0 leaves the resize and the rotation out of the gate: use another size (and aspect ratio), and the rotation the camera path passes, in the capture and in Stage 2 alike. - Declare the
androidx.testdependencies andtestInstrumentationRunner(Stage 2 needs them too), copytemplates/GoldenCaptureTest.ktintoapp/src/androidTest/java/<package>/, make it call the app's existing inference entry point (the class the UI calls, with its own preprocessing) on the fixed input on the CPU (no GPU or NNAPI delegate, XNNPACK at its default); when that entry point returns only a thresholded or sorted list, first give the class a function returning the raw output and one returning the preprocessed input (preprocess(bitmap): Stage 2 calls it too), still on the legacy runtime. Then run the four commands in its header: they install both APKs, run the test on the device, and pullgolden.txtintoapp/src/androidTest/assets/golden/output.txt. A raw tensor fed straight to the model is not a golden output: it leaves the preprocessing out of the gate. - Delete the capture test. Stage 2 compares against the file, so the test APK never needs
org.tensorflow:tensorflow-lite.
Step 1: Clean & Modernize Dependencies
Inspect libs.versions.toml and build.gradle.kts:
- Remove Legacy & Deprecated:
org.tensorflow:tensorflow-lite/tensorflow-lite-gpu/tensorflow-lite-select-tf-ops(Legacy Flex Delegate — see Deprecated API Remediation below), and every otherorg.tensorflow:tensorflow-lite*line, including ones other libraries pull in (./gradlew :app:dependencieslists them): the LiteRT artifacts carry theorg.tensorflow.literuntime classes and the model schema under their original names, and mixing old and new artifacts fails the build with duplicate classes.com.google.ai.edge.litert:litert-gpu1.x goes too (the renamed TFLite GPU delegate, last 1.4.2;litert-gpu:2.3.0is the GPU accelerator of the CompiledModel runtime and is added below), andcom.google.ai.edge.litert:litert1.x (the renamed Interpreter runtime, e.g.samples/litert_interpreter/*on 1.0.0) becomeslitert:2.3.0below.
- Swap the Support library coordinates (apps on
org.tensorflow.lite.support.*):org.tensorflow:tensorflow-lite-supportorlitert-support1.x becomesimplementation("com.google.ai.edge.litert:litert-support:2.3.0")(the Support API plus the LiteRT runtime: it depends onlitert:2.3.0);tensorflow-lite-support-apibecomeslitert-support-api:2.3.0(the API alone, the runtime being thelitertline below);tensorflow-lite-metadataorlitert-metadata1.x becomeslitert-metadata:2.3.0(MetadataExtractor). Then rename the package in every source file and in R8/ProGuard rules (-keep class org.tensorflow.lite.support.**):grep -rl 'org\.tensorflow\.lite\.support' --include='*.java' --include='*.kt' app/src/ | xargs sed -i 's/org\.tensorflow\.lite\.support/com.google.ai.edge.litert.support/g'(GNU sed; on macOS,sed -i '').org.tensorflow.lite.DataType, the type the Support classes take, keeps its name. - Add Modern LiteRT:
- Standalone:
implementation("com.google.ai.edge.litert:litert:2.3.0")andimplementation("com.google.ai.edge.litert:litert-gpu:2.3.0")in Kotlin DSL (implementation 'com.google.ai.edge.litert:litert:2.3.0'and'…:litert-gpu:2.3.0'in Groovy).litertpulls inlitert-apiand sets minSdk 24.litert-support:2.3.0depends onlitert:2.3.0and raises alitert:2.2.0pin to 2.3.0 without a warning (Gradle resolveslitert:2.2.0 -> 2.3.0), so pin 2.3.0 here. Since 2.3.0 the GPU accelerator (libLiteRtClGlAccelerator.so) ships inlitert-gpu; in 2.2.0 it was insidelitert. Withoutlitert-gpu,CompiledModel.Options(Accelerator.GPU, Accelerator.CPU)creates and runs on the CPU with the log lineGPU accelerator could not be loaded and registered, andOptions(Accelerator.GPU)alone throws.
- Standalone:
- Toolchain the 2.3.0 artifacts need:
litert-api:2.3.0depends onandroidx.lifecycle:lifecycle-runtime:2.10.0(its POM), and its classes carry Kotlin 2.4 metadata (2.2.0: Kotlin 2.3). Set these before the first build on a project on AGP 8.3 / Kotlin 1.9 (e.g.samples/litert_interpreter/*), or each becomes a build error in turn:compileSdk35 and AGP 8.6.0 (checkDebugAarMetadata), Gradle 8.7 (AGP 8.6.0), the Kotlin plugin 2.3 or newer (from 1.9 this is a plugin upgrade;freeCompilerArgs += "-Xskip-metadata-version-check"only lets a project already on 2.2 stay there, assamples/litert/image_segmentation/kotlin_cpu_gpudoes; 2.2 otherwise stops atThe binary version of its metadata is 2.4.0, expected version is 2.2.0, and 2.2 is enough for the 2.2.0 artifacts), and with Kotlin 2.x a Compose app needs theorg.jetbrains.kotlin.plugin.composeplugin in place ofcomposeOptions. On AGP 9 (built-in Kotlin) the Kotlin version is pinned withid("org.jetbrains.kotlin.android") version "2.3.21" apply falsein the root plugins block. - Labels and metadata:
FileUtil.loadLabelsandMetadataExtractorwork as before on the 2.3.0 artifacts. An app without the Support library reads the label file from assets: a.tflitewith metadata is a zip, so extract it once (unzip -o model.tflite labels.txt -d app/src/main/assets/;unzip -llists the name). - IDE Portability: Remove hardcoded
org.gradle.java.homefromgradle.propertiesand excludelocal.properties. - Kotlin Compiler DSL: Use top-level
kotlin { compilerOptions { ... } }outsideandroid { ... }:kotlinOptions { jvmTarget = "1.8" }becomeskotlin { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) } }.
Step 2: Deprecated API & Delegate Remediation
The agent must audit and replace all deprecated delegate APIs:
-
NNAPI Delegate (
NnApiDelegate,NnApiDelegate.Options,setUseNNAPI(true)):- Status: NNAPI is deprecated since Android 15, and the LiteRT 2.x Java/Kotlin API has no NNAPI entry point (
Interpreter.Optionshas nosetUseNNAPI). - Remediation: Remove
org.tensorflow.lite.nnapi.NnApiDelegateimports. Replace withCompiledModel.Options(Accelerator.NPU)andEnvironment.create(context, BuiltinNpuAcceleratorProvider(context))whenBuiltinNpuAcceleratorProvider(context).isDeviceSupported()is true (NPU-only options compile as NPU + CPU, so unsupported ops fall back to the CPU). In 2.3.0isDeviceSupported()is true on every Qualcomm, MediaTek or Samsung SoC from Android 12 and on Google Tensor from Android 16 (2.2.0 used a SoC list), and without the vendor librariescreatestill returns: the runtime logsFailed to apply compiler plugins: No compiler plugin found(2.2.0 addsFailed to initialize Dispatch API) and compiles for the CPU, so a cascade's reported accelerator reads NPU while the CPU runs. Implement an explicitNPU -> GPU -> CPUfallback cascade (try eachCompiledModel.create, catchLiteRtException) to handle non-NPU hardware smoothly. Under Q2 [B], an NNAPI choice in the app's UI becomes the GPU option (Options(Accelerator.GPU, Accelerator.CPU)).
- Status: NNAPI is deprecated since Android 15, and the LiteRT 2.x Java/Kotlin API has no NNAPI entry point (
-
Flex Delegate (
FlexDelegate,org.tensorflow.lite.flex,select-tf-ops):- Status: Interpreter-only: the Flex delegate ships in the legacy
tensorflow-lite-select-tf-opsartifact (68 MB for arm64 in 2.16.1) and in no LiteRT 2.x artifact, so a model with Select TF ops needs the re-export below. - Remediation:
- Remove
org.tensorflow:tensorflow-lite-select-tf-opsfrombuild.gradle.kts. - Find Flex ops by FlatBuffer inspection (custom ops whose
custom_codestarts withFlex);check_gpu_compatibilityinutilities/litert_gpu_toolkit(this repository) reports such a model as a compile failure, not op by op. - Replace Flex ops by re-exporting the model without them (LiteRT-Torch for PyTorch sources; the TensorFlow converter without
SELECT_TF_OPS), or, if custom C++ math is required, implement alitert::CustomOpKernel(litert/cc/litert_custom_op_kernel.h, shipped inlitert_cc_sdk.zip; guide: https://developers.google.com/edge/litert/next/custom_op_dispatcher).
- Remove
- Status: Interpreter-only: the Flex delegate ships in the legacy
Step 3: Native Build Toolchain (CMakeLists.txt / NDK)
For native C++ modules, use the C++ SDK the LiteRT docs describe (https://developers.google.com/edge/litert/next/android_cpp_sdk): download litert_cc_sdk.zip from the LiteRT GitHub release that matches the AAR version (https://github.com/google-ai-edge/LiteRT/releases/download/v2.2.0/litert_cc_sdk.zip; 2.3.0 has no GitHub release as of 2026-10-07, so a native module stays on the 2.2.0 AAR and zip until the matching zip exists, and an app with both a native module and the Support library waits for that zip, since litert-support:2.3.0 raises the runtime to 2.3.0), copy libLiteRt.so from the AAR's jni/arm64-v8a/ into its litert_cc_sdk/ directory (LITERT_CC_SDK below), and link the litert_cc_api target. The AAR ships no headers or prefab package, and liblitert_jni.so is the Kotlin binding, not a C++ target:
cmake# Replace legacy tensorflowlite_jni with the LiteRT C++ SDK add_subdirectory("${LITERT_CC_SDK}" "${LITERT_CC_SDK}/build") include_directories("${LITERT_CC_SDK}") target_link_libraries(your_native_lib litert_cc_api android log )
Step 4: API & Lifecycle Refactoring (Rewrite Initialization & Dynamic Signatures)
[!IMPORTANT] Never Simple Swap: Do NOT merely perform a search-and-replace of the
Interpreterclass. Rewrite the model initialization logic to instantiateCompiledModelwith an explicit hardware fallback cascade (NPU -> GPU -> CPU). When NPU is selected, prioritize NPU JIT compilation by instantiating an explicitEnvironmentobject (Environment.create(context, BuiltinNpuAcceleratorProvider(context))) and passing it toCompiledModel.create; on a supported device the provider itself setsEnvironment.Option.DispatchLibraryDirandEnvironment.Option.CompilerPluginLibraryDirtocontext.applicationInfo.nativeLibraryDir, and thecontextoverload enables the compiler cache undercontext.cacheDir.
| Legacy TFLite API | Modern LiteRT V2 Drop-in Replacement |
|---|---|
org.tensorflow.lite.Interpreter | com.google.ai.edge.litert.CompiledModel |
Interpreter(modelFile, options); LiteRT Support's Model.createModel(context, name, options) | CompiledModel.create(context.assets, assetName, options, env) or create(filePath, options, env); no ByteBuffer overload (via NPU Environment & Fallback Cascade) |
interpreter.run(input, output) | compiledModel.run(inputBuffers, outputBuffers) (both List<TensorBuffer> from createInputBuffers() / createOutputBuffers()) |
interpreter.runForMultipleInputsOutputs(inputs, outputs); Support model.run(inputs, outputs) | compiledModel.run(inputBuffers, outputBuffers) |
interpreter.runSignature(inputs, outputs, key) | compiledModel.run(inputMap, outputMap, key) (Map<String, TensorBuffer>) |
Interpreter.Options().setNumThreads(n) | options.cpuOptions = CompiledModel.CpuOptions(numThreads = n) on a fresh Options(Accelerator.CPU), not the shared Options.CPU |
interpreter.resizeInput(...) | none in 2.3.0 (resizeInputTensor is on LiteRT main after 2.3.0; the C++ API has ResizeInputTensor) |
GpuDelegate() / NnApiDelegate() | CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) / CompiledModel.Options(Accelerator.NPU) (NPU-only compiles as NPU + CPU) |
org.tensorflow.lite.flex.FlexDelegate | none: re-export the model without Select TF ops, or a C++ CustomOpKernel (Step 2) |
interpreter.getInputTensor(0).shape() | compiledModel.getInputTensorType(inputName).layout?.dimensions: by name only, no index or list call; args_0 / output_0 for LiteRT-Torch exports, the graph's tensor names otherwise (images / Softmax in the EfficientNet samples; print interpreter.getInputTensor(0).name() in the Step 0 capture test) |
imageProcessor.process(TensorImage.fromBitmap(bitmap)) then interpreter.run(tensorImage.buffer, out) | the same ImageProcessor, on com.google.ai.edge.litert.support.image; bridge.write(inputBuffers[0], tensorImage) (templates/SupportBridge.kt: writeFloat(tensorImage.tensorBuffer.floatArray) for a float image, writeInt8 for uint8, after a size check) |
Support TensorBuffer.createFixedSize(shape, DataType.FLOAT32) as the output, then TensorLabel / TensorProcessor | bridge.read(outputBuffers[0], shape, DataType.FLOAT32); TensorLabel / TensorProcessor unchanged |
#include "tensorflow/lite/interpreter.h" | #include "litert/cc/litert_compiled_model.h" |
#include "tensorflow/lite/c/c_api.h" | #include "litert/c/litert_compiled_model.h" |
Reference implementation: templates/LiteRtModel.kt (needs androidx.core:core-ktx and org.jetbrains.kotlinx:kotlinx-coroutines-android; it already carries Upgrades 2.A and 2.B, and closes everything it created when a step after CompiledModel.create throws). Copy it next to the app's inference class, replace the package, and keep its shape:
LiteRtModel.create(context, assetName, wantNpu, wantGpu = true, numThreads = null)is asuspendfactory that runs on the model's own serial dispatcher: it takes the process-wideEnvironment(created once, withBuiltinNpuAcceleratorProviderwhen the first caller wants the NPU andisDeviceSupported()), triesCompiledModel.createon NPU, then GPU + CPU, then CPU (wantGpu = falseskips the GPU step for an app whose CPU setting means the CPU alone;numThreadsis the legacysetNumThreads), catchingLiteRtException, and creates the buffers once.suspend fun run(FloatArray): FloatArraydoeswriteFloat/run/readFloaton the same dispatcher;run(write, read)takes two lambdas over the input and output buffer for the Support bridge below;runSync(input)is the entry point for Java callers and for Q3 [B] (the synchronous baseline), from a background thread;close()closes the buffers, then the model (the sharedEnvironmentstays open: creating one loads the accelerator libraries, and one created and closed per model reloads them on every load), and is called after the lastrunhas returned.acceleratoris the step that did not throw, not proof that every op runs there: NPU-only options compile as NPU + CPU, and a missing or failing compiler plugin is logged and skipped insidecreate; passwantNpu = trueonly with the libraries of Upgrade 2.C in place.Bitmap.toModelInput(size)in the same file is the preprocessing of an app without the Support library:androidx.core.graphics.scaleand one loop writing(channel - mean) / stdinto aFloatArray, with the legacyNormalizeOp(mean, std)values (127.5 / 127.5 in the template; change them to the app's). An app that keeps LiteRT Support leaves itsImageProcessoras it is, deletesBitmap.toModelInput, and copiestemplates/SupportBridge.kttoo:SupportBridge(model, inputName, outputName), created once aftercreate, reads the byte size of both tensors, and itswrite/readmove aTensorImageor SupportTensorBufferinto the model's buffers and back, and refuse one of another size or type (the Kotlin API itself accepts a shorter array, or uint8 bytes in a float input, without an error). Both libraries name a classTensorBuffer; the file imports the Support one asSupportTensorBuffer.
kotlinval model = LiteRtModel.create(context, "model.tflite", wantNpu = false) val scores = model.run(bitmap.toModelInput(224)) val bridge = SupportBridge(model.model, "images", "Softmax") val image = imageProcessor.process(TensorImage.fromBitmap(bitmap)) // LiteRT Support, as before val output = model.run({ bridge.write(it, image) }, { bridge.read(it, intArrayOf(1, 1000), DataType.FLOAT32) }) val scoresByLabel = TensorLabel(labels, output).mapWithFloatValue
Other one-to-one replacements: FileUtil.loadMappedFile(context, name) + Interpreter(buffer, options) becomes CompiledModel.create(context.assets, name, options, env) (loadMappedFile itself stays for MetadataExtractor); Interpreter.Options().addDelegate(GpuDelegate()) becomes CompiledModel.Options(Accelerator.GPU, Accelerator.CPU) (GPU alone makes create throw when an op is left undelegated, Some ops are not accelerated. Add kLiteRtHwAcceleratorCpu … from LiteRtCompiledModelT::Create in compiled_model.cc, and when the GPU cannot compile the graph at all, Failed to compile model; the legacy delegate left such ops on the CPU. GPU + CPU can still throw on a graph the GPU cannot compile, which is why the template keeps a CPU step after it); interpreter.run(inputByteBuffer, outputByteBuffer) becomes writeFloat / run / readFloat on the model's buffers (writeInt8 / readInt8 for int8 and uint8 tensors).
Step 5: Two-Stage Fast Verification Gate (Karpathy Self-Test)
To maximize execution speed:
- Stage 1 (Refactoring Gate): Run fast incremental compile checks only (
./gradlew compileDebugKotlin compileDebugAndroidTestKotlinor./gradlew assembleDebug assembleDebugAndroidTest) to verify syntax in seconds. Done when both source sets compile and the onlyorg.tensorflow.liteimports left areDataTypeandorg.tensorflow.lite.schema.*(grep -rn 'import org.tensorflow.lite' app/src | grep -v -E 'lite.DataType|lite.schema'prints nothing: noInterpreter, no delegate, noorg.tensorflow.lite.support). - Stage 2 (Final Verification Gate): Copy
templates/MigrationValidationTest.ktintoapp/src/androidTest/java/<package>/(model inapp/src/main/assets/), build (./gradlew assembleDebug assembleDebugAndroidTest) and run it on a connected device or emulator (./gradlew connectedDebugAndroidTest);testDebugUnitTestruns onlysrc/test.- The test runs the Step 0 input through the app's preprocessing (its
ImageProcessoron LiteRT Support, exposed as a function such aspreprocess(bitmap): TensorImage, or the function that replaced it, e.g.Bitmap.toModelInput) into the model's first input on the CPU (CompiledModel.Options(Accelerator.CPU)) and compares the first output with the golden file: max abs diff below 1e-3 when the file holds every output value (a tolerance for outputs of order 1; scale it for larger outputs), or the same top-1 index when it holds one (a last resort: a wrong normalization can keep the top-1). - Keep the test's own CPU model rather than calling
LiteRtModel, which tries NPU and GPU first. Done when it passes on a device and the app's own entry point has run there once (the gate executes neither the cascade nor the app class); with no device attached, stop after Stage 1 and open the PR as a draft that says Stage 2 did not run.
- The test runs the Step 0 input through the app's preprocessing (its
2. Phase 2: Encouraged Performance Upgrades
Once Phase 1 compiles and passes self-testing, the agent applies high-value performance features:
Upgrade 2.A: Off-main-thread execution on one serial dispatcher
The Kotlin CompiledModel API has no runAsync. On the CPU, run() blocks until the outputs are ready; on the GPU it can return before the GPU finishes, and readFloat() blocks until it has. Keep the UI responsive by running create, run and read on one serial dispatcher from a coroutine, and reuse the buffers created once:
kotlinprivate val modelDispatcher = Dispatchers.IO.limitedParallelism(1) suspend fun infer(input: FloatArray): FloatArray = withContext(modelDispatcher) { inputBuffers[0].writeFloat(input) compiledModel.run(inputBuffers, outputBuffers) outputBuffers[0].readFloat() }
Time run() and readFloat() together; timing run() alone under-reports GPU work. limitedParallelism(1) serializes the calls; it does not pin them to one thread.
Upgrade 2.B: Buffer reuse and zero-copy interop
In Kotlin, tensor buffers come from the model (createInputBuffers() / createOutputBuffers(), or createInputBuffer(name)), are filled with writeFloat / writeInt8, and are reused for every inference; there is no TensorBuffer.createFromAhwb in the Kotlin API. AHardwareBuffer zero-copy interop is part of the C++ API (litert::TensorBuffer::CreateFromAhwb in litert/cc/litert_tensor_buffer.h), so a camera pipeline that needs it runs the whole inference from C++ (Step 3): there is no supported way to hand a C++ buffer to Kotlin run().
kotlinval inputBuffers = compiledModel.createInputBuffers() val outputBuffers = compiledModel.createOutputBuffers() compiledModel.run(inputBuffers, outputBuffers) inputBuffers.forEach { it.close() } outputBuffers.forEach { it.close() } compiledModel.close()
Close the buffers, then the model, then the Environment if the app created one and no other model uses it (the model closes only an environment it created itself; templates/LiteRtModel.kt keeps one per process open).
Upgrade 2.C: NPU JIT Acceleration & Conditional INT8 Quantization
- Conditional INT8 Quantization: If NPU JIT is selected and the user opted in, run AI Edge Quantizer (
aeq) to generatemodel_int8.tfliteinassets/:
(ai-edge-quantizer 0.9.0 names; it pinspythonfrom ai_edge_quantizer import quantizer, recipe qt = quantizer.Quantizer("src/main/assets/model.tflite", recipe.static_wi8_ai8()) calibration = qt.calibrate({"serving_default": representative_inputs}) # an iterable of {input_name: ndarray} qt.quantize(calibration).export_model("src/main/assets/model_int8.tflite")ai-edge-litert2.2.0 on the host, which says nothing about the app's runtime version.static_wi8_ai8is the full-integer recipe that theaccuracy-safe-quantizationskill in this repo names for NPU targets;recipe.dynamic_wi8_afp32()needs no calibration but keeps float32 activations. Then point the app atmodel_int8.tfliteand rerun the Stage 2 gate with a golden of the top-1 index only; the 1e-3 bound is for the like-for-like float model.) - NPU JIT Runtime Bundling: Package vendor shared libraries in
app/src/main/jniLibs/arm64-v8a/(libLiteRtDispatch_Qualcomm.so,libLiteRtCompilerPlugin_Qualcomm.so,libQnnHtp.so,libQnnSystem.so, etc.);BuiltinNpuAcceleratorProviderreads them fromcontext.applicationInfo.nativeLibraryDir. Compiled artifacts are cached undercontext.cacheDirbyEnvironment.create(context, …)(enableCompilerCachedefaults to true). - Qualcomm FastRPC Permission: The
litert-apiAAR's manifest (pulled in bylitert; in 2.2.0 both AARs carry it) already declares<uses-native-library android:name="libcdsprpc.so" android:required="false" />(with the OpenCL and Google Tensor entries) and manifest merging brings it into the app; add it by hand only if merging is disabled. - Environment & Fallback Cascade: Create the
EnvironmentwithBuiltinNpuAcceleratorProvider(on a supported device it setsDispatchLibraryDirandCompilerPluginLibraryDiritself) and implement the KotlinNPU -> GPU -> CPUcascade / C++ fail-fast pipeline.
3. Phase 3: Automated Pull Request Provisioning
Once self-testing succeeds:
- GitHub Pull Request: Create a clean git commit, exclude
local.propertiesand.gradle/, and rungh pr createwith an attached before/after summary diff and test execution logs.


