摘要:把 LiteRT-LM 接到 App 进程内走 QNN/HTP 真·出字,理论上在 Snapdragon 8 Gen 2/3 上能跑;但当目标是车机 SoC SA8295P(Hexagon V73)时,会撞上 Google 2026-01 公开承认的"SA8295P not ready yet"。本文把从注册 Qualcomm ID、下载 QAIRT、推库、对接 LiteRT-LM、到 NPU 真·出字判据的全过程按时间线复盘一遍,附原始证据与下一步可破局路径。
目标:在车机 e6142550(Snapdragon SA8295P,Hexagon V73,30 TOPS NPU)上跑通 google-ai-edge/LiteRT-LM + QNN 端侧 NPU,让 1.5B Qwen2.5 文本模型在 HTP 上真·出字(不是静默回退 CPU)。
App 内嵌 LiteRT + QNN + Qwen 拆解 Qwen 是 AI 模型本身;LiteRT 是模型的 “调度总管”;QNN 是高通 NPU 的专属驱动插件;全部打包进你的车载 App 进程内部一起跑,不需要 Ollama、不需要 Linux 容器、不需要 root。
上层:Koog / MCP Agent(你的业务层,处理对话、工具调用)
↓ 调用推理接口
LiteRT(调度层)
↓ 绑定后端
QNN SDK库(NPU驱动插件)
↓ 驱动硬件
SA8295 HTP NPU(芯片硬件)
↑
Qwen模型文件:被LiteRT加载,通过QNN在NPU上执行
## 和 A.I chroot+Ollama+Qwen 做对比(方便理解)
### A.I(POC)
用户Query → Koog/MCP Agent → HTTP请求 → Ollama服务(跑在chroot Linux容器)→ Ollama加载Qwen → CPU计算
* Ollama:一站式大模型服务,自带 http 服务、模型管理;
* 缺点:独立服务进程,容器环境,root 依赖,只能 CPU 跑。
### D.II(量产)
用户Query → Koog/MCP Agent → 直接调用本进程内LiteRT接口 → LiteRT转发任务到QNN → NPU硬件跑Qwen
* **没有 Ollama,没有独立服务,没有容器**;
* 推理引擎直接内嵌 App 内部,NPU 硬件加速。
## 关键误区澄清
1. ❌ LiteRT 不是跑模型算力的,它只是调度;算力全部交给 QNN+NPU。
1. ❌ QNN 不能单独直接读取原始 Qwen 模型,必须 LiteRT 做模型加载、数据预处理。
1. ❌ Qwen 模型不能直接在 NPU 上跑,必须提前用 QNN 工具链离线编译,生成适配 HTP 的模型包。
已有条件:
com.fawcar.aiosagent.data.llm.LiteRTLLMClient)litertlm-android:0.17.0 aar(Google Maven).litertlm 模型LlmRouter)入口:https://myaccount.qualcomm.com/profile(注:这是浏览器侧入口,AI 工具不可直接登录,仅作步骤记录)
这是个一次性账号,免费;预研阶段不会触发付款,也不会触发 Qualcomm AI Stack License 的 redistribution 义务。
入口:https://softwarecenter.qualcomm.com/(需登录 Qualcomm ID)
2.51.40.260930(比文档上写的 2.47 更新;实测可用)lib/aarch64-android/ —— App 进程用的 QNN runtime(libQnnHtp.so 等)lib/hexagon-v73/unsigned/ —— DSP 端 skeleton(libQnnHtpV73Skel.so)⚠️ 关键坑:QAIRT 公开 Community 版的 HTP stub 是消费级 Snapdragon 代际,不一定匹配 SA8295P 的 HTP v7。SA8295P 真要量产,匹配 lib 须 OEM/车厂 BSP 固化。
D:\faw\project\aios\qnn\
└── v2.51.40.260930\qairt\2.51.40.260930\lib\
├── aarch64-android\ ← App 进程用
│ ├── libQnnHtp.so
│ ├── libQnnHtpPrepare.so
│ ├── libQnnHtpV73Stub.so
│ ├── libQnnSystem.so
│ ├── libQnnGenAiTransformer*.so
│ ├── libQnnModelDlc.so
│ └── ... (13 个核心 .so)
└── hexagon-v73\unsigned\ ← DSP 端 skel
└── libQnnHtpV73Skel.so
Backend.NPU() 真正拿到正确 lib 路径坑 1(linker namespace):App 是 adb 装到 /data/app/... 的非系统 App,其 linker namespace 不含 /vendor/lib64,所以 dlopen("/vendor/lib64/libQnnHtp.so") 会失败。 → 把 nativeLibraryDir 从硬编码 /vendor/lib64 改成 applicationInfo.nativeLibraryDir。
坑 2(Koin Scope):androidContext() 是 Scope/KoinApplication 的扩展,只能写在 single { } 内;写 module { } 顶层会编译报 "receiver type mismatch"。
坑 3(EngineConfig 误设):EngineConfig 不要给文本模型设 visionBackend / audioBackend,会 NOT_FOUND: TF_LITE_AUDIO_ENCODER_HW not found。
修改后 AppModule.kt 关键段:
// Koin named single,只在 single { } 内才能用 androidContext()
single<String>(named("npuLibDir")) {
androidContext().packageManager
.getApplicationInfo(androidContext().packageName, 0).nativeLibraryDir
}
single<LLMClient>(named("litert")) {
KoogLlmFactory.createLiteRTClient(
BuildConfig.LITERT_MODEL_PATH,
get(named("npuLibDir")), // ← App 自己的 lib/arm64
androidContext().cacheDir.absolutePath,
)
}
single { LlmRouterImpl(
npuClient = get(named("litert")),
npuModel = Models.localNpuQwen,
npuParams = Models.localChatParams,
npuAvailable = {
LiteRTLLMClient.NpuAvailabilityCheck.isAvailable(get(named("npuLibDir")))
},
...
) }
LiteRTLLMClient.kt 关键改动:
EngineConfig(
modelPath = modelPath,
backend = Backend.NPU(nativeLibraryDir), // ← nativeLibraryDir 已指向 App lib
// 文本模型不要设 visionBackend / audioBackend(设了 NOT_FOUND)
cacheDir = cacheDir,
)
nativeLibraryDir# 取 App 装包路径(每次重装 hash 变,必须重推)
adb shell pm path com.fawcar.aiosagent
# → /data/app/~~HASH==/com.fawcar.aiosagent-HASH==/base.apk
# lib dir = 上面的路径换 /base.apk 为 /lib/arm64
DEST=/data/app/~~.../com.fawcar.aiosagent-.../lib/arm64
SRC='D:\faw\project\aios\qnn\v2.51.40.260930\qairt\2.51.40.260930\lib\aarch64-android'
for f in libQnnHtp.so libQnnHtpNetRunExtensions.so libQnnHtpOptraceProfilingReader.so \
libQnnHtpPrepare.so libQnnHtpProfilingReader.so libQnnHtpV73CalculatorStub.so \
libQnnHtpV73Stub.so libQnnSystem.so libQnnGenAiTransformer.so \
libQnnGenAiTransformerModel.so libQnnModelDlc.so libQnnHta.so libQnnHtaNetRunExtensions.so; do
adb push "$SRC/$f" "$DEST/$f"
done
# DSP 端 skel(OEM 已固化在 /vendor/lib/rfsa/adsp/,无需 push):
# /vendor/lib/rfsa/adsp/libQnnHtpV73Skel.so
设备侧验证:
$ adb shell ls -l /data/app/.../com.fawcar.aiosagent-.../lib/arm64/libQnn*.so | wc -l
13
$ adb shell ls /vendor/lib/rfsa/adsp/
# adsp_callback_skel.so ... libQnnHtpV73Skel.so ...
.litertlm 模型# 台架 + PC 都到不了 huggingface.co(防火墙)
# 用 hf-mirror.com 镜像下载
curl -L -C - --noproxy '*' -o qwen2.5-1.5b-instruct.litertlm \
https://hf-mirror.com/litert-community/Qwen2.5-1.5B-Instruct/resolve/main/qwen2.5-1.5b-instruct.litertlm
# size = 1,597,922,856 bytes (1.6GB)
# 推到台架
adb push qwen2.5-1.5b-instruct.litertlm /data/local/tmp/
# build.gradle.kts 里 BuildConfig.LITERT_MODEL_PATH 默认指向这个路径
坑 4(后台下载 2 分钟硬杀):用 run_in_background=true 下载会卡在 2m1s 判 failed(数据还在流)。 → 用 curl -C - 断点续传 + for 循环 40 次(每次间隔 3s),前台给大 timeout。
LlmMode = LOCAL_ONLY# LlmMode.DEFAULT = CLOUD_ONLY,且 DataStore 已持久化
# 改代码 default 不生效(永远读存储值)→ 必须 clear
adb shell pm clear com.fawcar.aiosagent
坑 5(DataStore 持久化陷阱):这是最容易忽略的——ChatPrefsRepository 里有存储值时永远读存储值,改 LlmMode.DEFAULT 不会生效。
$env:GRADLE_USER_HOME = "D:\faw\project\aios\.gradle_uh"
& "D:\faw\project\aios\.gradle_uh\wrapper\dists\gradle-8.8-all\...\gradle-8.8\bin\gradle.bat" `
-p D:\faw\project\aios\AiosAgent :app:assembleDebug --no-daemon --max-workers=1
adb -s e6142550 install -r -d -t app/build/outputs/apk/debug/app-debug.apk
坑 6(UI 自动化不可靠):软键盘弹出会重排布局(EditText bounds 在 y=208/288 两档跳变),且输入框右侧图标是麦克风不是发送。改用
am start启动专用探针 Activity(100% 可靠)。
// NpuProbeActivity.kt(探针,临时)
class NpuProbeActivity : Activity(), KoinComponent {
private val outFile get() = File(filesDir, "npu_probe.txt") // 必须写 App 私有 filesDir
// 探针结果写文件(写 /data/local/tmp 不行,SELinux 不让 App 写)
...
val client = LiteRTLLMClient(modelPath, libDir, cacheDir.absolutePath, backend = Backend.NPU(libDir))
val reply = withContext(Dispatchers.IO) { client.execute(prompt, model).textContent() }
outFile.appendText("REPLY_OK elapsed=${elapsed}ms len=${text.length}\n")
outFile.appendText("REPLY_TEXT=$text\n")
}
adb shell am start -n com.fawcar.aiosagent/.data.llm.NpuProbeActivity
sleep 60
adb shell cat /data/data/com.fawcar.aiosagent/files/npu_probe.txt
"REPLY_OK 出现 = NPU 生效"是错的,会被静默回退 CPU 骗。
唯一判据(logcat):
Replacing ... with delegate (DispatchDelegate) → NPU ✅Replacing ... with delegate (TfLiteXNNPackDelegate) → CPU ❌(静默回退)辅助判据:
/proc/<pid>/maps 里没有任何 libQnn*.so → 一定没上 NPU/proc/<pid>/maps 里没有任何来自 App lib/arm64 的 .so → 加速器都没加载adb shell logcat -c
adb shell am start -n com.fawcar.aiosagent/.data.llm.NpuProbeActivity
# 必须在 20s 内抓,否则被 com.fawcar.ai.assistant 崩溃循环 + wpa_supplicant 刷屏冲掉
sleep 20 && adb shell logcat -d | grep -E 'DispatchDelegate|XNNPackDelegate|litert_dispatch|npu_registry'
探针输出:
mode=CLOUD_ONLY
ROUTE client=DashscopeLLMClient
根因:LlmMode 默认 CLOUD_ONLY,且 DataStore 已持久化该值(见 Step 7)。 → pm clear 后变 mode=LOCAL_ONLY / client=LiteRTLLMClient。
Engine.initialize() 加载 QNN lib 失败logcat:
E AndroidRuntime: java.lang.UnsatisfiedLinkError:
dlopen failed: library "/vendor/lib64/libQnnHtp.so" not found
根因:见 Step 4 坑 1(linker namespace),App 拿不到 /vendor/lib64。 → 改 nativeLibraryDir = applicationInfo.nativeLibraryDir,把 13 个 QNN lib 推到 App 自己的 lib/arm64/(见 Step 5)。 → 加完后 /proc/<pid>/maps 能看到 libQnnHtp.so / libQnnSystem.so 等被 dlopen,libQnnHtp.so 加载成功。
createConversation 报 TF_LITE_AUDIO_ENCODER_HW not found探针输出:
PROBE_FAILED: LiteRtLmJniException: Failed to create conversation:
NOT_FOUND: TF_LITE_AUDIO_ENCODER_HW not found in the model.
根因:EngineConfig 给文本模型误设了 visionBackend / audioBackend = Backend.NPU(...),而 qwen2.5-1.5b 文本模型里没有这两个编码器。 → 移除 visionBackend / audioBackend 字段(见 Step 4 坑 3)。
探针输出(看起来成功):
REPLY_OK elapsed=29495ms len=263
REPLY_TEXT=I apologize, but you've written "I didn't think much about it." ...
真因(logcat 抓出来):
I tflite : Replacing 1291 out of 1409 node(s) with delegate (TfLiteXNNPackDelegate) ... ← CPU!
/proc/<pid>/maps 里没有任何 libQnn*.so、没有任何 App lib/arm64 下的 .so。 → **实际上跑的是 CPU(XNNPack)**,NPU 路径在 dispatch 加载这一步就静默回退了。 → Backend.NPU` 同样静默回退**,照样出字,只留一行 W/E 日志。
这次教训:写"出字了"三个字时必须先在 logcat 里 grep
delegate (...),否则在写博客的同时已经把"假成功"记下来了。
libLiteRtDispatch_Qualcomm.so(LiteRT→QNN 桥接)Backend.NPU() 内部要 dlopen 一份叫 libLiteRtDispatch_Qualcomm.so 的桥接库(在 nativeLibraryDir 里找), 该库不在 litertlm-android:0.17.0 aar 里(aar 只有一个 liblitertlm_jni.so,TFLite+XNNPack 全静态打进去), 也不在 QAIRT SDK 里(QAIRT 只有一个老 TFLite 用的 libQnnTFLiteDelegate.so)。
唯一来源:google-ai-edge/LiteRT GitHub Releaseslitert_npu_runtime_libraries_jit.zip(v2.2.0):
DEST=/data/app/.../com.fawcar.aiosagent-.../lib/arm64
curl -L -C - --noproxy '*' -O \
https://github.com/google-ai-edge/LiteRT/releases/download/v2.2.0/litert_npu_runtime_libraries_jit.zip
unzip -j -d extracted/ litert_npu_runtime_libraries_jit.zip \
'*qualcomm_runtime_v73/*libLiteRtDispatch*.so' \
'*qualcomm_runtime_v73/*libLiteRtCompilerPlugin*.so'
# 提取出:
# libLiteRtDispatch_Qualcomm.so 445,576 bytes (导出 LiteRtDispatchGetApi)
# libLiteRtCompilerPlugin_Qualcomm.so 724,016 bytes (导出 LiteRtCreateCompilerPlugin)
# 都是 aarch64-elf64,对应 Hexagon V73(SA8295P 这一代)
adb push extracted/libLiteRtDispatch_Qualcomm.so $DEST/
adb push extracted/libLiteRtCompilerPlugin_Qualcomm.so $DEST/
ABI / 版本互校全过:
LiteRtDispatchApi 56 字节布局(含 abi_header)与 aar 嵌入的 LiteRT 兼容 ✅logcat 关键证据:
I litert : [litert_dispatch.cc:160] Loading shared library:
.../lib/arm64/libLiteRtDispatch_Qualcomm.so
I litert : [qnn_manager.cc:175] Loading qnn system shared library from ".../libQnnSystem.so"
W litert : [qnn_manager.cc:360] Qnn System library version 1.15.0 is used.
The version LiteRT using is 1.11.0. ← 版本 OK
W litert : [qnn_manager.cc:262] Qnn library version 2.40.0 is used.
The version LiteRT using is 2.36.0. ← 版本 OK
W litert : [qnn_manager.cc:302] Qnn backend library version 5.51.0 is used.
The version LiteRT using is 5.47.0. ← 版本 OK
I lrt-qc : INFO: [Qnn] Initializing QNN backend for SoC model: 39 ← SA8295P 的 soc_id
W qnn : QnnDsp <W> Failed in loading stub: dlopen failed:
library "libQnnHtpV68Stub.so" not found ← 找 v68 stub
E qnn : QnnDsp <E> Failed to load skel, error: 4000
E qnn : QnnDsp <E> Transport layer setup failed: 14001
E qnn : QnnDsp <E> Failed to create transport for device, error: 4000
E litert : [dispatch_api.cc:145] Failed to initialize QNN backend
E litert : [dispatch_delegate.cc:131] Failed to initialize Dispatch API
翻译:
/vendor/lib/rfsa/adsp/ 实际只有 v73 件)libQnnHtpV68Stub.so 失败 → skel 加载失败 → QNN backend 初始化失败 → 静默回退 CPU旁证:
Specified config SOC, ignoring on real target—— dispatch 内部硬编码 SoC 表里没 39,走兜底 v68。
把探针扩展支持 Backend.GPU(),三后端同 APK 同模型跑一遍:
| 后端 | 关键日志 | 结果 |
|---|---|---|
| CPU (XNNPack) | delegate (TfLiteXNNPackDelegate) + REPLY_OK 13.8s | ✅ 出字 263 字符 |
| GPU (Vulkan) | Failed to modify graph with delegate | ❌ 启动失败 |
| NPU (dispatch + QNN 2.51.40) | QnnDsp Failed to load skel, error: 4000 | ❌ DSP skel 加载失败 |
→ CPU 走通 + GPU/NPU 都失败,证明:模型本身 OK、LiteRT-LM 集成 OK、Koog 接线 OK、路由层 OK。卡点在 SoC 适配层。
HF litert-community/Gemma3-1B-IT discussion #20(原文链接):
"Unfortunately, SA8295P is not ready at the moment. Due to hardware differences from the existing supported SOCs, it will require a different quantization scheme to run. This SOC is on the team's radar and we will post the model here when SA8295P is supported." —— @marissaw, LiteRT Community (FKA TFLite) org, 2026-01-06
原始链接:https://huggingface.co/litert-community/Gemma3-1B-IT/discussions/20
含义:
v2.2.0 / v2.1.5 / v2.1.6 / v2.0.3 等所有公开 release 的 dispatch lib 内置的 SoC→代际表都把 SA8295P (soc_id=39) 强制走 v68。我们 push v73 库没用,是 dispatch 内部用 v68(兜底逻辑)。这条对所有想要在 SA8295P 上跑 NPU 的人都成立。
中间踩了 6 个独立坑(每个都能独立让 NPU 路径静默回退或崩):
LlmMode 默认 CLOUD_ONLY + DataStore 持久化 —— 改 default 不生效,必须 pm clear/vendor/lib64 对普通 App 不可 dlopen(linker namespace) —— 改推 App 自己的 lib dirandroidContext() 写在 module { } 顶层编译失败 —— 必须是 Koin single { } 内EngineConfig 误设 visionBackend/audioBackend —— 文本模型没有,NOT_FOUNDlibLiteRtDispatch_Qualcomm.so —— aar 不带、QAIRT 不带,要去 LiteRT GitHub Release 拿每个坑都"看似出字 = 成功",但只有 delegate 类型是 NPU 判据。这条最容易被写"成功"博客时漏掉。
方案 1:本地 bazel 编 dispatch + 覆盖 target_htp_arch=73(中等成本,最有可能跑通)
bazel build --config=android_arm64 @litert//litert/vendors/qualcomm/dispatch:dispatch_api_so--copt=-DQUALCOMM_TARGET_HTP_ARCH=73方案 2:OEM/车厂 BSP 提供 SA8295P 配套 QNN 运行时(高成本,走流程)
方案 3:等 Google 官方适配(无时间表)
| 阶段 | 证据 | 关键一行 |
|---|---|---|
| Step 5 | adb shell ls libQnn*.so | wc -l | 13 |
| Step 7 | mode=LOCAL_ONLY | pm clear 前后对照 |
| Step 9 | 探针文件 npu_probe.txt | REPLY_OK elapsed=29495ms len=263 |
| Step 9 | /proc//maps 无 libQnn*.so | "出字"=CPU,不是 NPU |
| Step 10 | logcat delegate 类型 | (TfLiteXNNPackDelegate) = CPU |
| 2.5 | dispatch lib 导出 | LiteRtDispatchGetApi / LiteRtCreateCompilerPlugin |
| 2.6 | QNN 互校 | Qnn System library version 1.15.0 ≥ 要求的 1.11.0 |
| 2.6 | SoC 映射错位 | SoC model: 39 + dlopen failed: library "libQnnHtpV68Stub.so" not found |
| 2.6 | QNN DSP 错误 | QnnDsp Failed to load skel, error: 4000 |
| 2.7 | 三后端对照 | CPU ✅ / GPU ❌ / NPU ❌ |
| 3.1 | Google 官方确认 | HF discussion #20, 2026-01-06 |
litert_npu_runtime_libraries_jit.zip)结论一句话:在 SA8295P 上跑 NPU 端侧 LLM,代码 + 库 + 桥接 dispatch 全部到位,仍然跑不出来——因为 Google 公开 dispatch 库把 SoC 39 强走 v68,而本机只有 v73。这不在我们控制范围内。业务上短期用 CPU 兜底,长期要么等 Google 适配、要么拿 OEM BSP、要么自己 bazel 编 dispatch 并覆盖 hex arch。