把 Qwen3-Reranker 搬上高通 NPU:在 FV01 上跑通 4096 token 重排序的全过程

weixin_46587843 2026-09-26 21:34:23

设备:FV01 开发板(高通 QCS8550,Ubuntu 22.04)|模型:Qwen3-Reranker-0.6B|精度:fp16
本文流程同样适用于犀牛派 X1 开发板。
代码与模型:https://github.com/zhouzengming/qwen3-reranker-qnn

写在前面

做 RAG 离不开 reranker:向量检索召回几十条候选,再由 reranker 精排,把真正相关的几条交给大模型。
Qwen3-Reranker-0.6B 体积小、效果好、支持中英文,很适合端侧。
这次的目标是:把它部署到 FV01 开发板的 Hexagon NPU 上,支持 4096 token 的长上下文,并对外提供标准的 rerank 服务。

先看最终结果:

项目结果
运行位置QCS8550 的 Hexagon v73 NPU(HTP),fp16
上下文长度4096 token(含 prompt 模板)
延迟满长度时每个 query–文档对 4.56 秒
精度与 fp32 PyTorch 的 logit 差值误差 ≤ 0.045,排序完全一致
服务FastAPI,兼容 Jina / Cohere 格式的 /v1/rerank,可接入 Dify 等应用

这条路走得并不顺。下面按时间顺序,把每个坑和解决办法都记下来。

一、整体思路

高通 NPU 上跑模型,最主流的路线是 QNN(Qualcomm AI Engine Direct,现在属于 QAIRT SDK):

PyTorch 模型 ──导出──▶ ONNX ──Qualcomm AI Hub 编译──▶ QNN context binary ──板端 QNN 运行时──▶ NPU
  • Qualcomm AI Hub 是高通的云端编译服务:上传 ONNX,它在云端编译成 NPU 能直接加载的 context binary,还能在真机上测性能、跑推理,非常方便。
  • 板子上用 QNN 的 C API 加载 context binary 并执行。

Qwen3-Reranker 的打分原理很简单:它本质上是一个语言模型。把 query 和文档套进固定的 prompt 模板,看模型在最后一个位置输出 "yes" 和 "no" 的概率:

score = P(yes) = softmax([logit_no, logit_yes])[1]

所以导出时不需要完整的 15 万词表输出,只保留 lm_head 的 yes/no 两行,模型直接输出两个 logit 即可。

二、第一站:512 token,先跑通

一开始先求稳:固定输入长度 512,把模型改写成静态形状。NPU 编译器只接受固定形状,所以输入统一左侧补齐到 512,attention mask 在图内生成。

导出 ONNX 后,用 onnxruntime 和官方 PyTorch 实现对比,误差在 1e-7 量级。接着在 AI Hub 上编译成 fp16,在 QCS8550 真机上实测:

  • 单次推理 201 ms,1669 个算子全部在 NPU 上;
  • 120 条中英文样本,正负样本的排序和 fp32 完全一致。

顺手做了个量化实验:结果得不偿失

按直觉,量化应该更快。我用 AI Hub 的量化功能做了 w8a16(权重 int8、激活 int16),用 400 条中英文检索数据校准,结果:

fp16w8a16
单次推理201 ms210 ms
与 fp32 的排序相关性(Spearman)0.99990.897

不但没变快,精度还明显下降,60 对正负样本里有 7 对排反了。在 v73 NPU 上,fp16 对这个模型是更好的选择,量化就此放弃。

一个小坑:排序要用 logit 差值,不要用概率

fp16 下,P(yes) 接近 1 时精度不够。比如两篇都很相关的文档,fp32 下分数分别是 0.99738 和 0.99721,到了 fp16 都变成了 0.99561,打成平手。
解决办法是用 logit_yes - logit_no(下文称 margin)排序。它和 P(yes) 单调对应,但不会在 1 附近饱和。改用 margin 后,60 对样本的顺序 100% 正确。

三、冲击 4096 token:三道坎

512 token 对长文档不够用,于是目标定为 4096。这一步花的时间最多。

第一道坎:注意力矩阵太大,NPU 内存放不下

直接把长度改成 4096,AI Hub 编译能通过(link 就花了近 2 小时),但一上真机就报:

QNN_COMMON_ERROR_MEM_ALLOC: Memory allocation related error

算一下就明白了:一层注意力的分数矩阵是 16 头 × 4096 × 4096 × 2 字节 = 512 MB,加 mask、softmax 时还会同时存在好几个这么大的张量。

解决:按 query 分块计算注意力。 softmax 是按行做的,每个 query 的计算和其他 query 无关,所以可以把 4096 个 query 切成 8 块,每块 512 个,依次计算再拼起来,结果和原版完全一样。
再利用因果性优化一下:第 i 块 query 只能看到前 (i+1)×512 个 key,后面的直接不算。

for start in range(0, L, 512):
    end = start + 512
    w = q[:, :, start:end] @ k[:, :, :end].T * scale      # 只取因果可见的 key
    w = softmax(w + mask[:, :, start:end, :end])
    out.append(w @ v[:, :, :end])

效果:最大的注意力张量从 512 MB 降到 64 MB,计算量还少了将近一半。
这个思路和 FlashAttention 类似,但只切 query 一个维度,不需要在线 softmax,改几行 PyTorch 代码就行,NPU 编译器可以直接处理。

第二道坎:分块后编译报错

分块后,28 层放在一张图里,link 阶段报了个很费解的错误:

graph_prepare.cc: Unable to find op in specified graph context

当时我判断是图太大、编译器处理不了,于是把 28 层切成 4 段,每段 7 层,各自编译成一个 context binary,推理时依次执行,段与段之间传递 hidden state。这也是高通部署大模型的常用做法。
(事后看,这个报错的真正原因是下面的第三道坎。不过分段让每个图更小、编译更快,也便于后面在板上管理内存,所以这个设计保留了下来。)

第三道坎:embedding 放在图里就编不过

切段后,第 2、3、4 段顺利通过,唯独第 1 段报同样的错误。第 1 段和其他段的唯一区别是:它包含 embedding 查表(输入是 token id,而不是 hidden state)。

为了确认,我用只有 1 层的小模型做了对照实验:只要图里有 4096 长度的 embedding 查表,link 基本都会失败(前后 7 次尝试失败了 6 次);去掉 embedding 后,4 次全部成功。

解决:把 embedding 查表挪到 CPU 上。 词表存成 fp16 的 .npy 文件(约 310 MB),用 mmap 加载,推理时按 token id 取出对应的行,交给第 1 段。这只是一次数组索引,耗时可以忽略。

至此,4096 token 的方案定型:

文本 ─分词/拼 prompt/补齐─▶ token id ─CPU 查 embedding─▶ hidden
      ─▶ 第1段(7层) ─▶ 第2段 ─▶ 第3段 ─▶ 第4段+输出头 ─▶ logit_yes − logit_no
                        (NPU,4 个 QNN context binary)

在 AI Hub 真机上测试:每段约 1.1–1.2 秒,合计约 4.56 秒。再在真机上把 4 段接力跑一遍,8 条样本(171–4096 token)和 fp32 的 margin 平均误差 0.019,正负样本顺序全部一致。

四、上板:在 FV01 上又踩了三个坑

AI Hub 上一切正常,不代表自己的板子上也能直接跑。我用 C++ 基于 QNN C API 写了一个轻量运行时(通过 ctypes 给 Python 调用),在 FV01 上一运行,又连续遇到三个问题。

坑 1:Unsupported SoC model: 66

Dsp startup: Unsupported SoC model (SnapdragonModel): 66

QNN 会从 /sys/devices/soc0/soc_id 读取芯片型号。FV01 的 soc_id 是 603,对应 QNN 里的 QCS8550(编号 66)。
QAIRT SDK 里有好几套 aarch64 Linux 运行库,我一开始为了兼容老版本 Ubuntu,选了 aarch64-oe-linux-gcc9.3,而这套库不支持 QCS8550。
查 SDK 文档里的支持设备表才发现:QCS8550 在 Linux 上只有 aarch64-oe-linux-gcc11.2 这一套库支持(要求 glibc ≥ 2.34,Ubuntu 22.04 正好满足)。换库后问题解决。

顺带一提:AI Hub 上的 "QCS8550 (Proxy)" 实际是按 SM8550(同样是 v73 NPU)编译的,生成的 context binary 在 QCS8550 上可以正常加载。

坑 2:FastRPC 权限

换好库,又报:

createUnsignedPD unsigned PD or DSPRPC_GET_DSP_INFO not supported by HTP
openSessionForPriority failed ... status=0x00000200

看起来像是"不支持 unsigned PD",但用 root 跑就没问题。用 SDK 自带的 qnn-platform-validator 验证:root 下通过,普通用户下失败。
原因是 CPU 和 NPU 通信靠 FastRPC 设备 /dev/adsprpc-smd,它的权限是 system:system 660,普通用户不在 system 组。

sudo usermod -aG system $USER   # 然后重新登录

坑 3:4 段模型放不进 NPU 内存

权限解决后,加载到第 4 段时报:

Failed to find available PD ... context size estimate 3876008960

在 L=4096 下,每段 context 约需 1 GB 的 NPU 内存,其中约 400 MB 是临时缓冲区(spill-fill)。4 段加起来超出了单个进程域(PD)的容量。我先后试了几种方案:

方案结果
把 4 个图 link 进一个 context 文件❌ 临时内存并不共享,预估 3.9 GB,还是放不下
Genie 用的 createFromBinaryListAsync + 共享资源❌ QCS8550 的 Linux 后端不支持
4 个独立 context 用 REGISTER_MULTI_CONTEXTS 分组加载✅ 加载成功,约 4 秒

最后一种方案下,QNN 日志显示第 4 段被自动放进了第二个进程域(pdId 2),执行时没有可测量的额外开销。

板上最终结果

$ python3 run_reranker.py selftest
tokens  ref_margin  npu_margin    diff  query
    85      7.5970      7.5781  0.0189  What is the capital of China?
    90    -10.3703    -10.3730  0.0027  What is the capital of China?
   104      7.3564      7.3828  0.0264  Explain gravity
    86      6.3610      6.3438  0.0173  中国的首都是哪里?
    86    -10.7718    -10.7266  0.0453  中国的首都是哪里?
  1759      5.8126      5.7969  0.0158  ppt策划案演讲
max |diff| = 0.0453 (tolerance 0.15) -> PASS

$ python3 run_reranker.py bench -n 5
  end-to-end (incl. host embedding/copies): median 4563.6 ms
  part1 execute: median 1107.1 ms
  part2 execute: median 1115.0 ms
  part3 execute: median 1122.9 ms
  part4 execute: median 1205.7 ms

精度和速度都和 AI Hub 真机的测量结果吻合,CPU 侧的额外开销只有约 13 ms。

五、服务化:一个兼容 Jina / Cohere 格式的 rerank 接口

模型跑通后,还要让应用方便地调用。OpenAI 官方 API 里其实没有 rerank 接口,业界通用的是 **Jina / Cohere 格式的 /v1/rerank**,vLLM、Xinference 都采用这种格式,Dify、FastGPT 等应用也都支持。

我用 FastAPI 实现了这个服务,有几个设计要点:

  • NPU 同一时间只处理一对 query–文档:推理放在只有一个线程的执行器里排队,事件循环不会被阻塞。排队期间 /health 仍然 30 ms 左右就能返回;
  • 只能开一个服务进程:每个进程都会加载一份模型,开多个会耗尽 NPU 内存;
  • 按 margin 排序,同时返回 0–1 的 relevance_score,兼顾兼容性和排序正确性。
$ curl http://127.0.0.1:8000/v1/rerank -H "Authorization: Bearer sk-xxx" -H "Content-Type: application/json" \
    -d '{"query": "中国的首都是哪里?", "documents": ["北京是中华人民共和国的首都。", "今天天气很好。"]}'

{"results": [{"index": 0, "relevance_score": 0.9982, "margin": 6.3438, ...},
             {"index": 1, "relevance_score": 0.0000128, "margin": -11.2686, ...}],
 "usage": {"total_tokens": 168}, "meta": {"elapsed_s": 8.846, "documents": 2}}

六、经验总结

  1. 先用短长度跑通,再挑战长上下文。 512 token 版本很快跑通,建立了完整的验证体系,后面 4096 的每一步改动都有对照基准。
  2. 每一步都要和 fp32 对比数值。 从 ONNX 导出、AI Hub 编译到板端运行,每一层都和 PyTorch 官方实现对比,问题出在哪一层一目了然。
  3. fp16 在 v73 上很香,量化不一定划算。 对于这个模型,w8a16 既不快、精度又差。
  4. 长序列的关键在注意力内存。 按 query 分块,是数学等价、实现简单、编译器友好的解决办法。
  5. 看清运行库和芯片的对应关系。 QCS8550 在 Linux 上必须用 aarch64-oe-linux-gcc11.2 这套 QNN 库。
  6. 别忘了 FastRPC 权限。 普通用户要加入 system 组才能访问 NPU。
  7. 多 context 要分组加载。 否则每个 context 都会预留自己的临时内存,很快就会超出 NPU 进程域的容量。
  8. 网络不稳时,大文件传输要加总超时。 AI Hub 的 SDK 传文件没有总超时,经过代理时可能半死不活地一直挂着。我在脚本里给所有数据传输都加了单次时限和断点续传。

七、快速上手(FV01 / 犀牛派 X1)

项目已经开源,Release 页面提供了编译好的模型,不需要 AI Hub 账号。

在 PC 上组装部署包:

git clone https://github.com/zhouzengming/qwen3-reranker-qnn.git && cd qwen3-reranker-qnn
# 1. 从高通官网下载 QAIRT SDK 2.50.0,解压到 qairt/ 目录(见 qairt/README.md)
# 2. 从 Releases 页面下载模型包,然后:
sha256sum -c qwen3-reranker-0.6b-qcs8550-L4096-fp16.tar.sha256
python3 export/make_deploy.py --prebuilt qwen3-reranker-0.6b-qcs8550-L4096-fp16.tar \
    --qairt-sdk qairt/<SDK 目录> --out dist/deploy

在开发板上运行(把 dist/deploy 拷到板子上):

sudo usermod -aG system $USER        # 首次使用:获取 NPU 访问权限,然后重新登录
cd deploy
pip install -r requirements-device.txt
source setup_env.sh
python3 run_reranker.py selftest     # 看到 PASS 就说明部署成功
python3 serve_reranker.py --host 0.0.0.0 --port 8000 --api-key sk-你的密钥

如果想自己从头编译(比如换一个上下文长度),执行 bash scripts/run_pipeline.sh 即可走完"导出 → 验证 → AI Hub 编译 → 打包"的全流程。
遇到问题可以先运行 bash tools/diagnose_npu.sh,再对照仓库 docs/NOTES.md 里的排错表排查。


项目地址:https://github.com/zhouzengming/qwen3-reranker-qnn
模型下载:https://github.com/zhouzengming/qwen3-reranker-qnn/releases/tag/v1.0.0

...全文
52 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文围绕基于物理场的动态模式分解(piDMD)展开研究,重点探讨其在流体力学、热传导及多物理耦合系统中的建模与应用。通过Matlab代码实现,将传统的动态模式分解(DMD)方法与物理场控制方程相结合,引入物理约束以增强数据驱动模型的物理一致性,从而提升对复杂非线性系统的模态提取精度与长期预测能力。文章系统阐述了piDMD的理论基础、数学推导过程与算法实现步骤,并结合典型物理场仿真案例(如Navier-Stokes方程驱动的流场演化)进行验证,展示了其在降阶建模、主导模态识别和时空动态可视化方面的优越性能,适用于高维、强耦合、非平稳系统的分析与优化。; 适合人群:具备一定数值计算、流体力学、控制理论或数据驱动建模背景,熟悉Matlab编程与线性代数运算,从事科学研究、工程仿真或系统辨识工作的研究生、科研人员及工程师。; 使用场景及目标:①实现对流场、温度场等物理场的高效降阶建模与动态行为预测;②提取符合物理规律的关键模态,用于系统稳定性分析与控制设计;③支撑复杂系统的状态估计、异常检测与优化调控等下游任务。; 阅读建议:建议读者结合提供的Matlab代码逐行调试与运行,深入理解piDMD算法中物理约束的嵌入方式及其对模态质量的影响,同时可尝试将其迁移至其他偏微分方程描述的物理系统中进行拓展验证与应用。

7,720

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、IoT、PC 、XR、Auto等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
物联网人工智能开源 企业社区 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧