Environment setup
Same setup as lesson 01: LiteRT-LM 0.18.0 (com.google.ai.edge.litertlm:litertlm-android:0.18.0 on Android, the LiteRTLM Swift package on iOS), Xcode 27 or Android Studio, and a real device. Mac scripts run with uv run in the litert-ship project (Python 3.11+).
You also need the Xcode command line tools for nm and otool: xcode-select --install.
1. Privacy manifest for required-reason APIs
Apple asks every app and third-party SDK to declare the "required reason" APIs it uses in a privacy manifest (PrivacyInfo.xcprivacy). Issue #3863 found that the LiteRT-LM iOS binaries (the CLiteRTLM.xcframework for v0.17.1 and the Metal prebuilt dylibs) used these APIs and shipped no manifest:
| Binary | Category found in #3863 | Reason codes the reporter suggested |
|---|---|---|
CLiteRTLM.framework | File Timestamp, System Boot Time | C617.1 (and 3B52.1 if users pick files), 35F9.1 |
libLiteRtMetalAccelerator.dylib | File Timestamp, User Defaults | C617.1, CA92.1 |
libLiteRtTopKMetalSampler.dylib | User Defaults | CA92.1 |
The issue was closed after PR #3910 merged on 10 Oct 2026, which is after v0.18.0 (6 Oct). We could not confirm from the release notes that v0.18.0 includes a manifest. So check the build you ship:
APP=build/MyApp.xcarchive/Products/Applications/MyApp.app
find "$APP" -name 'PrivacyInfo.xcprivacy' -print
for bin in "$APP"/Frameworks/*.framework/* "$APP"/Frameworks/*.dylib; do
[ -f "$bin" ] || continue
echo "== $bin"
nm -u "$bin" 2>/dev/null | grep -E '_(stat|fstat|lstat|mach_absolute_time)$|NSUserDefaults' | sort -u
done
If a binary uses one of these APIs and has no manifest of its own, add the categories to your app's PrivacyInfo.xcprivacy. Pick the reason codes that are true for your app. The table above lists the ones the reporter suggested, not a legal answer.
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key><string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key><array><string>C617.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key><string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key><array><string>35F9.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key><string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key><array><string>CA92.1</string></array>
</dict>
</array>
2. Duplicate Objective-C classes
Issue #3864 (still open) reports that libLiteRtMetalAccelerator.dylib and libLiteRtTopKMetalSampler.dylib each contain the same 35 Objective-C classes (GTMLogger, GTMLog*, GIPLog*, GIPSafe*, GSCError*, SRL*). The GPU path loads both, so the runtime prints 35 "Class X is implemented in both" warnings and uses one copy at random. If your app also links Google Toolbox for Mac, you get a third copy of the GTM* classes.
- Run the GPU path once on a device with the Xcode console open, and count the "is implemented in both" lines.
- If your app links Google Toolbox for Mac (often through another Google SDK), test logging from both sides.
- Write the count in your release notes, so a new warning in a future build stands out.
3. Large memory maps in a big app
Issue #2545 showed a Gemma 4 E2B model where one 1.28 GB section could not get a contiguous memory map in a large host app, even with about 3 GB free. The engine still said ready. Then inference returned empty output or crashed. The same model worked in a small sample app.
- Test the model inside your full app, after the user has used other heavy features, not only in a sample app.
- Treat empty output as an error. Log it and fall back.
- Try the Extended Virtual Addressing entitlement and measure. A commenter suggested it, but the reporter never confirmed it fixed the problem. See Fit the model in the memory budget.
4. Long context on the iOS GPU
From lesson 04: #3913 is on iOS, on an iPhone 17 Pro, with v0.18.0. Run the parity set on your oldest and newest supported iPhone. For prompts above 2048 tokens, use activationDataType: .float32 or CPU unless your parity report says float16 is fine.
import LiteRTLM
func engineConfig(modelPath: String, promptTokens: Int, gpuAllowed: Bool, maxTokens: Int) throws -> EngineConfig {
guard gpuAllowed else {
return try EngineConfig(modelPath: modelPath, backend: .cpu(), maxNumTokens: maxTokens)
}
let activation: ActivationDataType = promptTokens > 2048 ? .float32 : .float16
return try EngineConfig(modelPath: modelPath, backend: .gpu,
maxNumTokens: maxTokens, activationDataType: activation)
}
Release checklist
- Privacy manifest covers every category the LiteRT-LM binaries use.
- Duplicate class warnings counted and written down.
- Model tested inside the full app after heavy use. Empty output handled.
- Parity report passed on oldest and newest supported devices.
- Stop-then-leave test from lesson 01 passed 100 times.
maxNumTokensset from the bundle, near-limit test passed (lesson 03).
Sources
- Apple: Privacy manifest files
- Apple: Extended Virtual Addressing entitlement
- LiteRT-LM pull request #3910
Key concepts
- #3863: LiteRT-LM iOS binaries used required-reason APIs with no privacy manifest. A fix merged after v0.18.0.
- #3864: two Metal dylibs define the same 35 Objective-C classes.
- #2545: a large model section can fail to map in a big app, and the engine can still say ready.
- #3913: long prompts on the iOS GPU need float32 or CPU unless your tests say otherwise.
Takeaways
- Scan your archive for manifests and required-reason symbols before every release.
- Test the model inside the full app, not only in a sample.
- Keep one written checklist and run it for every LiteRT-LM update.