高通 QNN 全景拆解(二):从模型量化到多模型并发的完整部署实战

这是「高通 QNN 端侧 AI 部署全解析」系列的第二篇。上一篇我们搞懂了 QNN 的软件栈分层。今天来实战 — 从一个 HuggingFace 上的模型出发,一路走到 NPU 上跑起来的完整链路。

先看全流程:

端侧 LLM 部署 6 步流水线:从 HuggingFace 模型到 NPU 上机推理的完整流程,附量化方案选型矩阵

Step 1:模型获取

从 HuggingFace 下载预训练模型。推荐 SafeTensors 格式(比 PyTorch .bin 更安全,支持 mmap 加载):

# 安装 HuggingFace CLI
pip install huggingface-cli

# 下载 Llama 3.1 8B (约 16GB FP16)
huggingface-cli download meta-llama/Llama-3.1-8B-Instruct \
  --local-dir ./llama3-8b \
  --include "*.safetensors" "config.json" "tokenizer*"

# 或者用国内加速的 Qwen2.5 7B
huggingface-cli download Qwen/Qwen2.5-7B-Instruct \
  --local-dir ./qwen25-7b

模型选型建议:

模型 参数量 FP16 大小 INT4 大小 推荐场景
Llama 3.1 8B 8B ~16GB ~3.5GB 通用对话、代码
Qwen2.5 7B 7B ~14GB ~3.2GB 中文场景首选
MiniCPM 3B 3B ~6GB ~1.5GB 低内存设备
Phi-3.5 Mini 3.8B ~7.6GB ~1.8GB 推理能力强

端侧部署目前主流就是 3B-8B 这个区间。3B 以下能力不够用,8B 以上内存扛不住。

Step 2:量化压缩

FP16 模型太大,8B 模型 16GB,手机内存塞不下。量化是必经之路。

# 使用 QNN 转换器进行 W4A16 量化
qnn-onnx-converter \
  --input_network ./llama3-8b/model.onnx \
  --output_path ./llama3-8b-w4a16.dlc \
  --quantization_overrides quantize_config.json \
  --act_bw 16 \
  --weight_bw 4 \
  --bias_bw 32 \
  --float_fallback

--float_fallback 很关键 — 它让不适合量化的算子(比如 LayerNorm、Softmax)保持 FP16/FP32 精度,避免精度崩塌。

量化方案选型矩阵:

方案 权重精度 激活精度 7B 模型大小 精度损失 Decode 速度 推荐场景
W4A16 ★ 4-bit 16-bit ~3.5GB < 2% ~25 tok/s LLM 首选
W8A8 8-bit 8-bit ~7GB < 1% ~15 tok/s 高精度需求
W8A16 8-bit 16-bit ~7GB < 0.5% ~18 tok/s 精度敏感
W4A8 4-bit 8-bit ~3.5GB 3-5% ~28 tok/s 极致速度
W16A16 (FP16) 16-bit 16-bit ~14GB 0% ~8 tok/s 基线对比

W4A16 是目前最佳平衡点 — 权重 4-bit 压缩 4 倍省内存,激活保持 16-bit 维持精度。大部分 LLM 在 W4A16 下的 PPL(困惑度)增加不到 0.5,人类几乎感知不到差异。

反直觉:W4A8 虽然速度更快,但在中文场景精度下降明显(5%+)。 因为中文 embedding 的分布比英文更稀疏,8-bit 激活容易截断长尾信息。做中文 LLM 别用 W4A8。

Step 3:图编译

量化完的 .dlc 还不能直接跑,需要让 QNN 做图编译:

# 生成模型库 (.so)
qnn-model-lib-generator \
  --model ./llama3-8b-w4a16.dlc \
  --backend libQnnHtp.so \
  --lib_targets aarch64-android \
  --output_dir ./model_libs/

# 输出: llama3_8b_w4a16.so (包含编译后的计算图)

这一步 QNN 会做:

  • 算子融合:MatMul + BiasAdd + Activation 融合成一个 kernel
  • Graph Partitioning:自动把不支持的算子 fallback 到 GPU/CPU
  • 内存规划:计算每层的 buffer 大小,做 buffer 复用减少内存占用
  • 指令生成:把算子翻译成 HTP 硬件指令

Step 4:Context Binary 缓存

这步是冷启动优化的关键:

# 生成 Context Binary
qnn-context-binary-generator \
  --model ./model_libs/llama3_8b_w4a16.so \
  --backend libQnnHtp.so \
  --output_dir ./cache/ \
  --binary_file llama3_ctx.bin \
  --config_file htp_config.json

HTP 配置文件 htp_config.json:

{
  "graphs": {
    "vtcm_mb": 8,
    "soc_model": "SM8650",
    "performance_profile": "burst",
    "precision": "fp16",
    "enable_deep_learning_runtime": true
  }
}
参数 含义 建议值
vtcm_mb VTCM (L2 SRAM) 分配 8(最大化 SRAM 利用)
soc_model 目标 SoC SM8650 (8 Gen 3) / SM8750 (8 Elite)
performance_profile 性能模式 burst(最快)/ sustained_high(持续)
precision 计算精度 fp16(推荐)/ int8

Step 5:上机部署

把编译产物推到设备上:

# 推送模型和 QNN 运行库
adb push ./cache/llama3_ctx.bin /data/local/tmp/models/
adb push ./model_libs/*.so /data/local/tmp/qnn_libs/

# 推送 QNN SDK 运行库
adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtp.so /data/local/tmp/qnn_libs/
adb push $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtpV73Stub.so /data/local/tmp/qnn_libs/
adb push $QNN_SDK_ROOT/lib/hexagon-v73/unsigned/libQnnHtpV73Skel.so /data/local/tmp/qnn_libs/

# 配置环境
adb shell "export LD_LIBRARY_PATH=/data/local/tmp/qnn_libs:\$LD_LIBRARY_PATH"

Step 6:推理 + 性能调优

# 基础推理测试
adb shell "cd /data/local/tmp && \
  qnn-net-run \
    --backend libQnnHtp.so \
    --model models/llama3_ctx.bin \
    --input_list input_list.txt \
    --output_dir ./output/ \
    --perf_profile burst \
    --profiling_level detailed"

Profiler 输出会告诉你:

  • 每层的执行时间(找到瓶颈层)
  • NPU/GPU/CPU 的占比(确认 Partitioning 合理)
  • 内存峰值使用量
  • DDR 带宽利用率

性能基线(骁龙 8 Gen 3,Llama 3 8B W4A16):

指标 数值
Prefill (128 tokens) ~180ms
Decode 速度 ~25 tokens/s
首 Token 延迟 ~200ms
内存占用 ~4.2GB(含 KV Cache)
功耗 ~3W(NPU 单独)

📊 DDR 带宽瓶颈分析

很多人以为端侧 AI 的瓶颈是算力(TOPS 不够)。坦率的讲,骁龙 8 Gen 3 的 45 TOPS 对于 7B 模型是够用的。真正的瓶颈是 DDR 带宽。

LLM decode 阶段,每生成一个 token 需要读取全部权重一遍:

7B INT4 权重: ~3.5GB
目标 25 tok/s: 3.5GB × 25 = 87.5 GB/s 理论需求

LPDDR5x 4266MHz 实际带宽: ~50 GB/s (理论 68.2, 实际利用率 ~75%)

87.5 / 50 = 1.75 → 带宽不够!

为什么实际能跑到 25 tok/s?因为 QNN 做了几个优化:

  1. 权重分块加载 — 不是每个 token 都读全部权重,而是按 layer 分块,配合 DMA 预取流水线
  2. KV Cache 压缩 — GQA (Grouped Query Attention) 减少 KV Cache 带宽需求
  3. INT4 packing — 两个 4-bit 权重打包成一个字节,减少实际传输量

💡 这就是为什么骁龙 8 Elite 的 NPU 算力翻倍到 80 TOPS,但 decode 速度只快了 ~30% 而不是 100% — 因为瓶颈在带宽不在算力。LPDDR5x 没变,带宽天花板还在那。


   转载规则


《高通 QNN 全景拆解(二):从模型量化到多模型并发的完整部署实战》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
端侧大模型性能调优在高通芯片上实战 端侧大模型性能调优在高通芯片上实战
⚡ Profiling 三板斧 · Memory-Bound 破局 · 投机采样加速 前两篇聊了硬件架构和量化方法。到这一步,你应该已经有了一个量化好的模型,能在车机 NPU 上跑起来了。Qwen2.5-VL-3B 在 SA8397P 多核
2026-03-22
下一篇 
高通 QNN 全景拆解(一):从框架到硬件的完整软件栈 高通 QNN 全景拆解(一):从框架到硬件的完整软件栈
这是「高通 QNN 端侧 AI 部署全解析」系列的第一篇。两篇的结构: 本篇:QNN 全景 — 软件栈分层 + 三大后端 + API 核心调用 第二篇:部署实战 — 6 步上机流程 + 量化选型 + DDR 带宽优化 如果你在做端侧 A
2026-03-12
  目录