聊到大模型部署,很多人第一反应就是英伟达显卡、CUDA、vLLM,再不济也是苹果的Core ML、谷歌的TFLite。但如果你手里是一台高通平台的手机、智能座舱域控制器,或者一块带NPU的机器人开发板,情况就完全不一样了。这里没有CUDA,却有一套高通官方的神经网络推理工具链,名字叫QNN(Qualcomm Neural Network,高通神经网络)。我这两年陆续帮不少团队调过端侧大模型项目,凡是要把LLM跑在高通硬件上的,最后基本都会绕到QNN这门基础课上。
这篇文章我不打算给你抄官方文档,而是从实际部署的角度,把QNN是什么、在大模型场景里怎么用、有哪些绕不开的坑,以及一套能跑通的完整流程,一次性讲清楚。内容主要面向要做端侧大模型部署的工程师,也适合正在调研智能座舱、移动端AI落地方案的团队参考。看完你至少应该明白:QNN在高通平台上扮演什么角色,为什么大模型部署绕不开它对NPU的调度能力,以及怎么把一个普通ONNX模型改造、量化、编译成能在高通货上运行的格式。
1. QNN到底是什么,为什么大模型绕不开它
1.1 高通异构计算的统一入口
高通平台上的AI计算历来是“多核协同”的状态:CPU负责通用逻辑,Adreno GPU负责图形和部分并行计算,而真正的神经网络加速主力是Hexagon DSP,尤其是里面的HTP(Hexagon Tensor Processor)张量处理器。早期高通给开发者提供的SDK叫SNPE,用得也还行,但问题是它和TensorFlow、ONNX等框架的衔接比较死板,算子支持也经常跟需求脱节。
QNN是SNPE的下一代统一方案,在高通眼里它叫“AI Engine Direct”。它把CPU、GPU、DSP(NPU)全部抽象成统一的QNN算子图和运行时后端。也就是说,不管你的模型是从PyTorch导出的ONNX,还是TensorFlow导出的TFLite,最后都会被转换成QNN自己的中间表示,再根据目标硬件编译成可执行的二进制。对于大模型来说,最大的价值在于:QNN提供了针对int8、int4等低比特数据的硬件加速路径。大模型动辄几B到几十B的参数,想在设备端跑起来,几乎必须走低比特量化,而高通NPU对这类计算的调度能力,恰恰是QNN要解决的核心问题。
这里多说一句:很多人容易把QNN和高通的“AI Hub”混淆。AI Hub更像是一个模型仓库和自动化评测平台,而QNN是真正落地的推理SDK。你可以在AI Hub上找预置模型,也可以拿它跑基准测试,但真正要把自己的模型集成进产品,你还是得回到QNN工具链上。
1.2 QNN与大模型的关系:端侧LLM落地的一座桥
说到大模型,我们通常想到的是A100、H100这些数据中心显卡。但大模型端侧化和私有化部署的需求这两年增长非常快,比如车载语音助手、本地知识库问答、会议纪要生成、边缘端数据过滤等场景,都不希望把数据传到云端。这种需求之下,高通这套平台的优势就出来了:功耗低、集成度高,手机上、汽车座舱里都有现成的算力。
问题在于,大模型不是普通的卷积神经网络,它有大量的Transformer结构,核心算子是矩阵乘法(MatMul)、Softmax、LayerNorm、GELU这些。高通NPU对卷积、Pooling这类算子的优化已经很成熟,但Transformer结构里的一些动态形状和张量操作,早期工具链支持得并不好。QNN近几年做了不少针对Transformer的算子增强和编译器优化,比如更高效的Softmax近似实现、针对多头注意力的内存布局优化,以及对动态形状的有限支持。所以,现在想在高通平台上跑LLM,QNN虽然不是唯一路径,但绝对是最值得优先考虑的官方路径。
另外一个容易被忽视的点是:大模型的部署不只是“把模型塞进NPU”这么简单,还需要处理Tokenization、采样策略、KV Cache、流式输出这些运行时逻辑。QNN并不直接管这些,它负责的是模型里算子的高效执行。实际产品往往是:外层用C++/Java做应用逻辑,中间层用QNN Runtime执行模型,KV Cache在设备内存里手动管理。理解了这个分工,再去读QNN文档就会顺很多。
1.3 QNN和其它推理框架的定位差异
我自己在选型时,经常被问到这样一个问题:QNN和ONNX Runtime、TensorRT这些框架有什么区别?其实它们的层级不太一样。ONNX Runtime是一个通用的跨平台推理引擎,它当然也能调度高通的NPU,但通常需要通过“EP(Execution Provider)”机制对接QNN。TensorRT是NVIDIA平台上的专用加速方案,和QNN在各自硬件上做的事情类似,但完全没法跨平台通用。TFLite、PyTorch Mobile这些更偏移动端通用框架,对NPU的利用通常依赖厂商的Delegate,到了高通平台上,最终还是会落到QNN上。
这么说吧,QNN更像是高通硬件之上的“驱动级推理框架”,它和硬件绑得最深,能榨出的性能也最高。其他框架是高通硬件上的“过客”,而QNN是“地主”。如果你的目标是把大模型部署到高通设备上,跳过QNN直接上层框架,可能会在性能上吃大亏,也可能根本跑不起来NPU。下表是我在实际选型中常用的对比,可以帮你快速定位:
| 框架 | 所属平台 | 对高通NPU的支持 | 适合场景 |
|---|---|---|---|
| QNN | 高通 | 原生,最深 | 高通设备上的高性能推理 |
| SNPE | 高通 | 原生但较老 | 维护老项目,新项目不建议 |
| ONNX Runtime | 跨平台 | 通过QNN EP | 需要跨平台统一调度的项目 |
| TFLite | 跨平台 | 通过DSP Delegate | 轻量模型的移动端集成 |
| TensorRT | NVIDIA | 不支持 | NVIDIA设备专属 |
| llama.cpp | 跨平台 | 部分社区支持 | 端侧CPU/GPU,暂未原生对接NPU |
2. 核心细节解析与实操要点
2.1 QNN工具链的组成
QNN SDK是一整套工具链,我第一次打开目录的时候也愣了会儿,文件实在太多。但主线其实就四条:模型转换器、模型编译器、运行时工具和调试分析工具。
模型转换器的核心是qnn-onnx-converter和qnn-tflite-converter。前者把ONNX模型转换成QNN的C++模型描述文件(.cpp),后者处理TFLite模型。转换出来的描述文件还不能直接跑,需要经过qnn-context-binary-generator编译,最终生成带权重和指令的上下文二进制文件(通常以.serialized.bin结尾)。真正跑推理的时候,用的是qnn-net-run这个命令行工具,或者把QNN Runtime集成进你的应用代码里。
有一件事我刚开始学时踩过坑:QNN的“backend”(后端)概念,libQnnHtp.so、libQnnCpu.so、libQnnGpu.so分别对应NPU、CPU、GPU三种执行后端。如果你生成上下文二进制时指定的后端是HTP,那跑的时候也必须加载HTP的backend库,否则会报错。而且HTP后端还分V73、V75、V79这些架构版本,不同代高通芯片对应不同架构版本,选错了直接加载失败。
还有一个工具叫qnn-model-tool,可以用来检查、修改模型里的张量形状和元数据,做动态形状研究时经常用到。这个工具有点像手术刀,功能很强大,但如果只是入门部署,暂时可以不用深入研究它。重点先把转换、编译、运行这条主链路走通,后面再逐步加入量化、性能分析这些进阶操作。
2.2 量化:大模型过NPU的第一道坎
大模型要跑在高通NPU上,量化是无法回避的问题。高通的HTP张量核心对FP16有一定的支持,但对int8、int4的支持才是其长项,尤其是int4,几乎是为LLM这类超大模型准备的。量化之所以难,是因为它是有损压缩,压缩得不好模型就“变笨”了。
QNN支持PTQ(训练后量化)和QAT(量化感知训练),大部分大模型落地场景先用PTQ就能解决。PTQ的流程是:用一批有代表性的输入数据(校准数据)跑一遍原模型,记录每一层激活值的统计分布,然后根据分布信息把浮点权重和激活映射到int8或int4范围。校准数据的选取非常关键。我见过不少团队随便拿一批随机噪声当校准数据,结果模型精度掉得没法看。正确的做法是拿真实测试环境中会出现的数据,比如对话场景就拿真实对话文本,让tokenizer转成id后丢给模型做校准。
QNN在量化时支持多种量化策略,比如per-tensor和per-channel、对称和非对称。大模型里权重分布差异很大,per-channel量化通常能更好保留精度。QNN也支持混合精度量化,比如注意力层的权重用int8,FFN层因为对精度不太敏感可以用int4,整个模型可以在精度和体积之间做比较精细的平衡。我在一个7B模型上做过实验,全int8大概能保住95%以上的精度,而混合int8/int4能进一步把体积压到4GB以下,精度损失还在可接受范围内。
这里有一个非常关键的注意事项:LLM中的激活值分布和CNN很不一样,尤其Softmax后面、LayerNorm输出附近,激活值可能集中在很小的范围内。如果直接用默认量化参数,这些小范围的数值很容易被粗糙地压成一样的值,导致输出完全崩掉。所以做量化时,一定要重点关注LayerNorm层和Softmax层,必要时把这些层保留为浮点计算,或采用更高比特的量化。QNN允许在graph里为个别节点指定不同的量化精度,这需要你比较仔细地跑几轮实验,找到最适合自家模型的量化配置。
2.3 图优化与context binary的角色
另一个容易困惑的概念是context binary。你可以把它理解成一个“打包好的可执行文件”,里面包含了模型图结构、算子指令、权重数据,以及后端运行时需要的一切信息。生成context binary之后,加载它并不需要重新解析模型图和权重,只要放进QNN Runtime里,就能以接近“裸执行”的方式在硬件上跑。
所以context binary有几个很明显的优点:加载速度快、运行时开销小、权重数据可以加密保护。但缺点也很突出:它和具体硬件、具体SDK版本强绑定。同一份模型在骁龙8 Gen 2上编译出来的context binary,拿到骁龙8 Gen 3上很可能跑不了,因为HTP架构版本不一样。你换了一版QNN SDK,最好也重新编译一遍,否则可能遇到不明所以的运行时错误。
图优化方面,QNN会在编译时做算子融合(比如把卷积后的激活函数融合进卷积节点)、内存布局优化、算子拆分等等。这些都不需要人工干预。需要人工关注的其实是大模型的动态形状问题。LLM的输入序列长度是动态变化的,而NPU更擅长处理静态形状。QNN对动态形状支持有限,常见的做法是限定形状范围,比如把输入长度分桶(bucket)处理,或者直接把解码阶段的最大生成长度固定下来,牺牲一部分灵活性换取执行效率。在跑QNN之前,先把这个策略想清楚,能省很多调试时间。
3. 实操:把一个大模型搬到QNN上跑通
3.1 准备环境与模型
实际操作前,先把环境准备好。你需要一台能跑QNN SDK的Linux x86主机(用于模型转换和编译),以及一台带高通芯片的设备(用于执行验证),设备上需要装有对应的QNN Runtime库。如果暂时没有真实设备,高通也提供了一些模拟器方案,但性能数据参考价值有限,建议还是尽早真机验证。
模型选择上,我的建议是先从小模型开始,别一上来就挑战7B、13B。端侧大模型部署的第一步,是用一个小模型把整条链路跑通,比如0.5B到1.5B级别的模型就很合适。这类模型规模小,转换、编译、调优都快,即便出错,定位问题也直观。等你把QNN的整个流程摸熟了,再把它迁移到更大的模型上,会顺很多。现在开源社区能找到不少支持ONNX导出的中小型语言模型,下载好之后先转成ONNX格式。如果你用的是PyTorch,可以利用torch.onnx.export接口,但注意要把模型的动态轴(比如序列长度)标记清楚,否则后面转到QNN时会很痛苦。
一个我在实战中总结的小经验:导出ONNX时,尽量让模型保持静态输入形状,或者在导出时就把形状范围固定好。QNN对动态形状的支持是“能用但有限”的状态,你越早把模型形状问题解决掉,后面越轻松。此外,大模型的原始权重是FP32或FP16,体积非常大,转换前可以先做一次预量化,比如直接把权重转成int8或者int4,这样会加快后续转换和编译速度。
3.2 模型转换与量化校准
拿到ONNX模型后,第一步是用QNN的转换器把它变成QNN的模型描述文件。以ONNX为例,命令大致是这样的:
# 进入QNN SDK目录 source env.sh # 转换ONNX到QNN C++模型描述 python ${QNN_SDK_ROOT}/bin/qnn-onnx-converter \ --input_model /path/to/model.onnx \ --output_dir /path/to/output \ --input_list /path/to/input_list.txt \ --calibration_data /path/to/calibration_data \ --calibration_data_reader /path/to/reader_script.py这里有几个参数需要解释一下。input_list.txt是输入张量的形状描述文件,每一行定义了一个输入的name、shape和数据类型。如果模型带动态轴,QNN转换器通常需要你在命令行上指定一个具体的形状范围。calibration_data目录放的是校准数据,也就是我在前面强调的真实样本。calibration_data_reader是一个Python脚本,告诉QNN怎么读取你的校准数据文件,这个脚本需要实现一个特定的接口,SDK文档里有模板可以参考。
经过这步,你会得到一个.cpp文件(或者一个包含多个文件的目录),这个文件描述的是QNN的“模型图”。如果你只是想在CPU上跑一下,可以跳过下一步直接编译。但如果目标是把大模型部署到NPU上,那就要进入context binary生成阶段了。
在进入编译前,有一个细节值得留意:请检查转换日志里有没有算子被降级到CPU。QNN的转换器有时会碰到不认识的算子,它会默认把该算子放到CPU后端执行,这在功能上是通的,但性能会非常差。尤其是大模型里如果某些算子在NPU上执行不了,整个模型可能频繁地在CPU和NPU之间切换,推理速度会断崖式下跌。所以我每次转换完,都会先扫一遍日志,用grep搜一下“failed”或者“partition”,确认关键算子都跑在NPU上。
3.3 生成context binary与设备端推理
接下来是生成上下文二进制:
# 生成context binary,指定HTP后端 python ${QNN_SDK_ROOT}/bin/qnn-context-binary-generator \ --model /path/to/output/model.cpp \ --backend libQnnHtp.so \ --output_dir /path/to/context_output如果你的模型经过量化,这一步还需要传入量化相关的配置。生成成功后会得到一个.serialized.bin后缀的文件,这个就是最终要部署到设备上的核心产物。文件里已经打包了图结构、权重和NPU指令,所以在设备端无需再安装模型转换工具链,只需要QNN Runtime库。
把生成的bin文件和运行时库推到设备上之后,通常先用QLC(Qualcomm Logic Code)或者qnn-net-run这个命令行工具验证一下:
# 设备端运行推理 ./qnn-net-run \ --model /data/local/tmp/model.serialized.bin \ --backend libQnnHtp.so \ --input_list /data/local/tmp/input_list.txt \ --output_dir /data/local/tmp/output这个命令会读取输入数据,跑一次推理,并把输出结果保存到指定目录。如果这是你第一次跑,可能会碰见各种错误,比如backend加载失败、memory权限不够、input list不匹配等等。这些我都遇到过大半,后面我会单独用一节讲排障。
等到命令行工具能跑通,你就可以着手把它集成到自己的应用里了。在代码里,你需要初始化QNN Runtime,加载context binary,然后通过QNN的API把输入数据送入模型并取回输出。高通SDK里提供了C++和Python的示例代码,建议先扒一扒示例代码,再结合自己项目的应用场景做二次开发。
3.4 性能调优的几个重要方向
模型跑通只是第一步,要真正落地,性能调优是绕不开的硬仗。大模型推理有两个重要阶段:prefill(处理输入prompt)和decode(逐个生成token)。一般来说,prefill阶段是计算密集型,NPU能发挥很大作用;decode阶段是内存访问密集型,每一轮都要把整个模型权重从DDR读一遍,这时NPU的算力反而不是瓶颈,内存带宽才是。所以很多端侧大模型的性能优化,往往围绕“如何减少decode阶段的内存访问”来展开。
在QNN平台上,可以尝试的方向包括:对权重做int4混合量化,减小内存读取量;尽量扩大decode阶段的batch size(如果业务允许同步处理多条请求时);合理设置KV Cache的内存池,避免动态分配带来的开销;以及利用QNN的graph batching能力,同时处理多个输入。不过具体能调到什么程度,和模型结构、设备型号、SDK版本都有关系,强烈建议每调一个参数,都跑一次端到端的延迟测试,不要把时间花在“感觉应该会变快”的事情上。
还有一个通用技巧:别一上来就追极致性能,先保证功能正确,再去一层层做性能优化。我在项目里通常的做法是,先把模型在CPU后端跑通,再切到NPU后端看性能,最后再做量化压缩和算子级的调优。这样每一步的改动范围都很小,出了问题也容易定位。
4. 常见问题与排查技巧实录
4.1 算子不支持与fallback
这是我在QNN上遇过最多的问题。ONNX模型里有很多算子,转换器不可能每个都原生支持。常见的不支持算子在Transformer结构里相对少,但也遇到过比如某些动态形状相关的算子、某些新版本ONNX里的高级索引算子,QNN转换器不支持,于是默认把它放到CPU上执行。
虽然功能上没错,但如果CPU算子在NPU热路径上,性能会非常难看。解决办法有几个:一个是修改模型结构,用QNN支持的算子组合去替代不支持的算子;一个是把不支持的算子“切”出主图,单独在CPU或GPU上执行;还有一个是升级SDK版本,新版工具链对算子的支持通常会扩大,有时候升级完同一份模型就全绿了。不过升级SDK也意味着要重新做一遍编译和平台适配,性价比要看具体情况。我一般建议先查官方文档里的算子支持列表,确认是不是静态版本问题,再决定是否升级。
4.2 量化精度下降怎么处理
量化后精度下降,是所有端侧大模型部署都会遇到的事,只是程度不同。如果发现模型变“傻”了,先不要急着上QAT,试一下调整PTQ的参数。
第一步,确认校准数据集是否贴近真实场景。校准数据至少要覆盖实际业务中会出现的文本类型,数量建议几百到几千条,别太少。第二步,检查量化策略,看是否可以用per-channel,或者对敏感层提高比特数。第三步,尝试混合精度,让LayerNorm和Softmax附近的关键层保持较高精度。第四步,也是我经常用的方法:观察每一层的激活值分布。如果某个层的激活分布极端集中,或者出现明显的离群点,重点调整那几层的量化配置,往往立竿见影。如果以上都试过还不理想,就该考虑QAT了,但这种方案训练成本高,工程周期长,是小体量团队的“绝招”,不是首选。
4.3 内存不足与批量大小
大模型的参数和中间激活值都很大,NPU上的内存有限,很容易碰到内存不足。最直接的解决办法是降低批量大小,或者缩短输入序列长度。但如果是decode阶段的内存不足,排查的方向就不太一样了。KV Cache是decode阶段最大的内存消耗源之一,它的占用和token数量成正比,需要根据模型结构和最大生成长度精确计算容量。QNN本身不管KV Cache的管理,你需要在自己的推理框架里为它分配一段内存,并在每次生成时更新其中的内容。
一个小技巧是:为KV Cache分配内存池时,一次性分配够最大需要的空间,避免运行中动态扩容。动态分配在端侧很危险,轻则性能抖动,重则直接崩溃。我在某个车型项目里就遇到过类似问题,后来改为静态内存池就好多了。
4.4 日志与调试手段
QNN的报错信息其实写得还可以,前提是你得把日志打开。环境变量QNN_LOG_LEVEL可以设置日志级别,从verbose到error都有。排查问题时的第一步,就是先把日志级别调到verbose,再复现一次,往往错误原因就写在日志里了。
最常碰到的几个报错,我整理了一张速查表:
| 报错现象 | 可能原因 | 解决思路 |
|---|---|---|
| Backend library not found | libQnnHtp.so路径不对 | 设置LD_LIBRARY_PATH或显式指定路径 |
| Arch mismatch / unsupported | context binary架构版本和设备不匹配 | 用匹配设备的HTP版本重新生成 |
| Input tensor mismatch | 输入形状或数据类型与定义不一致 | 检查input_list和实际数据 |
| Quantization param missing | 编译时未提供量化配置 | 补充量化参数,重新生成context |
| OOM / memory allocate failed | 内存池不够或动态分配导致 | 增大内存池,降低batch,静态分配KV Cache |
我自己在排查问题时还有一个习惯:先把模型在CPU后端跑一遍作为基线。如果CPU也是同样报错,说明问题出在模型转换或输入数据处理上,和NPU无关;如果CPU跑正常NPU报错,再集中精力查NPU相关的编译和内存配置。这个二分法能省很多时间。
5. QNN与大模型生态:下一步怎么玩
5.1 与Ollama、llama.cpp等生态的结合预期
Ollama和llama.cpp这类项目能火,是因为它们提供了一条极低门槛的本地大模型使用路径。但说实话,它们默认的运行路径还是偏向CPU和部分GPU,对高通NPU的支持远没有到开箱即用的程度。
不过社区一直在往这个方向努力。llama.cpp已经有一些针对Qualcomm后端的实验性分支,思路基本是把QNN作为llama.cpp的一个新backend,这样上层应用不需要改,模型算子会尽量调度到NPU执行。我试过几个分支,体验参差不齐,有些能跑通但性能不稳定,更多的是半成品状态。如果团队里有人力愿意折腾,可以考虑把这些分支拿下来试水;但如果是生产环境,建议还是走正规的QNN SDK方案,自己做一套推理运行时来管模型加载、KV Cache和采样逻辑。这样工程量大一些,但是可控性强得多。
5.2 高通生成式AI工具与AI Hub
高通这些年也在努力降低大模型端侧部署的门槛。除了QNN本身,它还推出了AI Hub平台,上面有一些预置的生成式AI模型和工具脚本,可以一键转换、编译、评估。如果你用的是比较主流的模型,比如Whisper、Llama系列,说不定在AI Hub上能找到现成的范例,省去前面好多步骤。
但AI Hub最值得参考的,反而是它展示的模型性能数据。在做技术选型时,你可以拿着这些数据大致评估“某个模型在高通某个平台上大概能跑到多少token/s”。虽然最终性能取决于你的具体场景,但有个基准总比盲人摸象强。另外,AI Hub还提供了自动量化和精度评估的流程脚本,你完全可以借鉴它的思路,搭一套自己的部署流水线。我自己就在这个基础上,加了一个自动回归测试的环节:每出一个新版本SDK,就跑一边完整测试集,确认精度和性能没有回退。这个做法帮我提前发现了很多潜在问题。
5.3 一个可落地的扩展方向:小模型+端侧RAG
QNN落地大模型,最实际的方向之一,是“小模型+端侧检索增强生成(RAG)”。为什么强调小模型?因为端侧NPU的存储和计算资源有限,强行部署一个大参数模型,体验往往很差。与其如此,不如用一个1B到3B级别的小模型,配合本地知识库做RAG,把回答质量提上去。
RAG流程里,文本怎么切块、向量怎么存、检索怎么排序,这些都可以在端侧完成。高通平台上一般可以用QNN跑embedding模型,或直接在CPU上跑轻量的向量检索。这套组合下来,既兼顾了隐私数据不上云,又能让大模型回答结合具体业务知识,比单纯塞一个大模型进来靠谱得多。我做过的一个驾驶助手项目,就是用2B模型加本地FAQ知识库,回答准确率比直接上7B模型不分上下,但推理延迟却降了好几倍。这个方案值得想落地的团队优先考虑。
最后再分享一个我自己的体会:QNN这门“基础课”之所以值得花时间去啃,是因为它代表了一个趋势——大模型正在从云端走进设备端,而高通的芯片是目前设备端最有存在感的平台之一。学QNN不需要你精通底层硬件,但一定要动手跑通一条完整的转换、编译、推理链路,因为很多细节,不踩一遍坑是记不住的。建议你先拿一个小模型,按这篇文章里的流程完整走一遍,再回到官方文档里逐项核对,理解会深刻得多。如果后续遇到具体问题,多看日志、多用二分法定位,就没什么过不去的坎。