MNN QNN 离线模型让骁龙 NPU 真正跑起大模型
同样是跑在 Hexagon NPU 上,自研 Hexagon 后端早期构建 decode 仅 1.5 t/s(4B App 实测;0.6B llm_bench 9.6 t/s),经过 pisces312/MNN fork 持续优化后,0.6B 已提升到 11.65 t/s(近 8 倍改善)。而高通官方 QNN SDK 路线却能跑出 32 t/s——仍是 Hexagon 的约 2.7 倍。这篇文章讲清楚两条 NPU 路线的本质区别、QNN 离线模型的使用方式,以及为什么它才是骁龙 NPU 跑 LLM 的最优姿势。
为什么有了 Hexagon 还要 QNN
MNN 有两条把计算 offload 到骁龙 NPU 的路线:
| MNN Hexagon(自研) | MNN QNN(官方 SDK) | |
|---|---|---|
| 代码位置 | source/backend/hexagon/ | source/backend/qnn/ |
| 后端类型 | MNN_FORWARD_HEXAGON(type=10) | MNN_FORWARD_NN(type=5) |
| 算子实现 | 自研 HVX/HMX kernel(htp-ops-lib) | 高通 QNN HTP runtime |
| 部署形态 | stub + skel 两个 so,FastRPC 调用 | 离线 context binary + plugin 算子 |
| KV cache 管理 | 无 HTP 图内 KV 管理;decode 每 token 全量权重经 DMA 流进 VTCM(受固件带宽墙限制) | HTP 图内管理,零搬运 |
| 适用场景 | 大 batch prefill(长文档总结) | decode(逐 token 生成) |
两条路线在硬件上最终都跑在 Hexagon DSP 上,但架构完全不同。Hexagon 后端是 MNN 自己写 DSP kernel、通过 FastRPC 逐个算子调用;QNN 后端是把整张图交给高通的 QNN runtime 编译和执行,KV cache、权重调度这些细节全部由 QNN 内部优化。
关键差异在 decode:Hexagon 是通用 HVX DSP,KV cache 不在图内管理,decode 每 token 都要把全量权重经 DMA 从 DDR 流进 VTCM(仅 8MB,放不下 300MB~2GB 权重);零售机固件在 NoC/DDR QoS 层对 unsigned PD 的 DMA 流量封顶 ~2.8GB/s,这是代码层无法突破的墙。QNN 则把 KV cache 放在 HTP 图内自生自灭,用专用 tensor 单元执行,decode 几乎不与 CPU 交互。这就是为什么同是 0.6B、同用 App 短对话口径,QNN decode(32 t/s)是 Hexagon(~1.5 t/s)的约 21 倍(若统一到 llm_bench,Hexagon 0.6B 仍有 9.6 t/s,QNN 优势约为其 3 倍+,QNN llm_bench 待补)。
在线构图 vs 离线构图:LLM 必须走离线
QNN 有两种使用模式:
- 在线构图:运行时把 MNN 图翻译成 QNN 图,即时编译执行。只支持静态形状的常规模型(如 CNN 分类/检测),对 LLM 这种动态形状 + 融合算子的模型直接崩溃。
- 离线构图:提前在 PC 上把模型编译成 QNN context binary(
.bin),运行时由 plugin 算子直接加载执行。LLM 只能走这条路。
一句话:App 设置里选npubackend = 在线构图(LLM 必崩);QNN 离线模型的backend_type是cpu,计算实际在 NPU 上由 plugin 算子驱动。初学者最容易在这里搞混。
离线模型是怎么生成的
Qwen3-0.6B QNN 离线模型的生成分四步:
- NPU 专用导出:
llmexport.py加--generate_for_npu --seperate_embed --disable_transformer_c4 --sym --omni --hqq --quant_bit 4 --quant_block 64 --act_bit=16。几个关键开关:
--generate_for_npu:在模型里传播 activation 量化信息--seperate_embed:embedding 和 lm_head 单独存,CPU 侧执行--disable_transformer_c4:关掉 Hexagon 专用的 C4 布局,QNN 不认识--omni:用 OmniQuant 做权重量化校准,精度比纯 HQQ 更好
- compilefornpu:把 MNN 模型编译成 QNN 图,按
--max_history_token 2048生成两档位(prefill 128 token / decode 1 token)。KV cache 内置在 QNN Attention 里。 - qnn-model-lib-generator:把 QNN 图编译成可加载的 context library。
- qnn-context-binary-generator:生成最终的 HTP context binary(
graph0.bin),这就是跑在 NPU 上的东西。
产物里,graph0.bin 是真正的 NPU 可执行文件(含所有权重和两档位图),llm.mnn 是一个约 2.3KB 的 plugin op 模型(含 state/seq_len 元数据),作用是告诉 MNN:"加载这个 bin 文件,剩下的交给 QNN"。
⚠️ 转换链路曾有两个导致真机首条消息 SIGSEGV 的 upstream bug,均已在本 fork(`https://github.com/pisces312/MNN`)修复:
1.compilefornpu整图路径_compileWholeModule不写state/seq_lenattr → KVclientBuf为空 →graphExecute返回 6004 →Llm::sample采样段错误;
2. 动态StridedSlice负 index 被 QNN 钳为 0 → 短 prompt logits 越界段错误;
3. 另含QNNStridedSlice负 index wrap 修复(已提交 upstream 级质量)。
用本 fork 的compilefornpu/ 已编译工具链即可直接跑通,无需手动打补丁;上述修复的细节见qnn-device-crash-analysis_20260809_1031.md。
真机性能:三方对比
同一台设备(荣耀 SM8850 / 骁龙 8 Elite Gen 5 / HTP V81),同一个模型(Qwen3-0.6B Q4 g64),三种后端实测:
| 指标 | CPU fp16 (4 线程) | Hexagon cDSP(0.6B / 4B) | QNN HTP (NPU) |
|---|---|---|---|
| Prefill (短) | 306 t/s | 223 / 48 t/s | 13~21 t/s |
| Decode | 70 t/s | 11.65 / 1.41 t/s(App) | 32 t/s |
| 首字延迟 (20 tok) | ~0.07s | ~0.11s | ~1.6s |
Hexagon 数字均为 [pisces312/MNN](https://github.com/pisces312/MNN) fork 当前版本 App 短对话实测。0.6B decode 从早期 ~1.5 t/s 提升到 11.65 t/s(近 8 倍),已进入可用区间;4B 受固件 DMA 带宽墙约束,改善有限(1.5 → 1.41)。llm_bench 口径下 0.6B 为 9.6 t/s(早期构建,fork 版待复测)。根因分析详见姊妹篇《从 1.5 到 11.65:MNN Hexagon DSP 后端让大模型跑上骁龙》。
怎么解读:
- Prefill 是 CPU 的强项:大矩阵乘法吃内存带宽,CPU 带宽优势明显。NPU 的 prefill 受 context binary 加载开销和 KV cache 初始化影响,短 prompt 下偏慢。但如果是几百 token 的长 prompt(文档总结、RAG),Hexagon 的 prefill 能到 2000+ t/s,远超 CPU。
- Decode 是 QNN 的主场:32 t/s ≈ 每秒 20+ 个中文字,完全可用。是 Hexagon 0.6B 的约 2.7 倍(32 vs 11.65),达到 CPU 的近一半。相比早期构建(Hexagon ~1.5 t/s,差距 21 倍),fork 优化后差距大幅收窄。
- Hexagon decode(0.6B)已可用,4B 仍受限:0.6B 当前 fork 达到 11.65 t/s(App 短对话),每秒约 7~8 个中文字;但 4B 仅 1.41 t/s。根因是零售设备固件在 NoC/DDR QoS 层对 unsigned PD 的 DMA 流量封顶 ~2.8GB/s——fork 通过优化算子调度将 0.6B 推到了带宽墙附近的极限,而 4B 权重总量更大、DMA 压力更高,改善空间有限(详见姊妹篇《骁龙手机端侧大模型低功耗推理:MNN Hexagon cDSP 后端实录》)。
- 模型越大,NPU 越赚:0.6B 上 CPU 还能赢,但到 4B 级 CPU decode 会因带宽瓶颈急剧下降,而 HTP 的 tensor 单元和片上缓存能维持稳定吞吐。4B 的 QNN 转换正在进行中。
Qwen3-0.6B QNN HTP App 实测截图(提问"NPU 是什么",输出 203 tokens):

Qwen3-0.6B QNN HTP 实测:decode 32.56 t/s
NPU 的真正价值:不是更快,是更可持续
性能之外,NPU 存在的核心理由是能效和不占 CPU:
| CPU fp16 | QNN HTP | |
|---|---|---|
| 典型功耗 | 3~5W(全核满载) | 0.5~1W |
| CPU 占用 | 占满所有核,UI 卡顿 | 几乎为零,UI 流畅 |
| 热降频 | 2~3 分钟后触发,速度断崖下跌 | 低功耗,长时间稳定 |
| 后台任务 | 挤占系统资源 | 完全不影响 |
对用户来说,NPU 的体验是:
- 长时间对话不卡:一场 10 分钟的深度对话,CPU 可能后半段因热降频明显变慢,HTP 全程稳定
- 边推理边干别的:模型生成回复时,你可以流畅刷网页、切 App、回消息
- 省电:同样的推理量,HTP 功耗只有 CPU 的 1/3~1/5
即使 0.6B 上 CPU 速度更快,如果你频繁使用、长时间对话,NPU 的体验反而更好。
怎么用
下载
不想自己编译 App? 直接从 GitHub Releases 下载预编译的 APK(v0.8.3.3),安装即可使用。APK 已内置 QNN + Hexagon 后端支持,无需从源码构建。
- Qwen3-0.6B QNN 离线模型:huggingface.co/pisces312-hf/qwen3-0.6b-mnn-qnn-npu
部署
把模型目录整个放到手机的 /sdcard/mnn-models/ 下,然后在 App 的"我的模型"里就能看到。
注意:在模型设置里 不要把 backend 改成 npu(那是在线构图入口,LLM 必崩)。QNN 离线模型的 backend_type 默认就是 cpu,plugin 算子内部会自动走 HTP——保持默认即可。
早期版本要求额外推送llm.mnn.weight(plugin 虽不读,但llm.cpp会无条件checkFile),现已通过 external weight 标记解决:HF 上发布的模型config.json均已更新,按目录整体推送即可,无需单独补权重文件。
写在最后
端侧 NPU 跑 LLM 这件事,不是"开个开关就变快"那么简单。Hexagon 后端和 QNN 后端是两条完全不同的技术路线,各有适用场景:
- 长文档总结、RAG 检索 → Hexagon prefill(2000+ t/s)
- 逐 token 对话生成 → QNN HTP decode(32 t/s,低功耗)
- 小模型短时使用 → CPU 直接上(最快,省心)
真正的端侧 AI 体验,不是把所有计算都扔给 NPU,而是根据工作负载的特点,在 CPU / Hexagon / QNN 之间做智能调度。MNN 的多后端架构恰好提供了这种灵活性——同一个模型、同一个 App、同一个引擎,切换 backend 就是一行配置的事。
---
*环境:MNN(2026-08 master)/ QAIRT SDK 2.39.0 / SM8850(骁龙 8 Elite Gen 5,HTP V81)/ Android 16*
更多推荐
所有评论(0)