
1. 从“跑不动”到“跑起来”大模型落地高通平台的现实困境这两年聊大模型部署绕不开一个尴尬场景云端跑得好好的模型一搬到端侧设备就“水土不服”。别说什么7B、13B参数量级的大模型就连一些轻量级模型在手机、车载、物联网设备上想达到可用帧率都是一场硬仗。我自己最开始接触高通平台做模型迁移时天真地以为只要把PyTorch模型导出成ONNX再转成高通SDK支持的格式就能完事结果跑起来才发现要么算子不支持要么内存暴涨要么精度掉得没法看。这一圈折腾下来我才真正意识到——高通QNNQualcomm Neural Network这套基础能力才是解决端侧大模型部署问题的关键钥匙。QNN是高通推出的统一神经网络推理框架用来替代老一代SNPESnapdragon Neural Processing Engine。它面向高通自家的CPU、GPU、Hexagon DSPNPU等异构计算单元提供统一的模型转换、量化、编译和推理接口。大模型要在高通平台上真正“跑起来”核心就是理解QNN的工作机制搞清楚模型从训练框架到端侧runtime之间到底经历了什么。这篇内容就是围绕这个主题把我踩过的坑、验证过的方法、梳理清楚的原理一次性讲透。这篇内容适合谁看如果你是做端侧AI部署的工程师、正在折腾高通平台的算法工程师或者刚入门想搞明白大模型在高通芯片上怎么落地的学生那这篇文章应该能帮你少走不少弯路。我会从QNN的整体架构讲起拆解大模型转换的完整链路细说量化、内存优化、算子支持这些关键环节再分享一些实操记录和问题排查经验。没有太多花哨的东西都是实打实能用的基础。2. 先搞懂大模型在端侧部署的完整链路2.1 大模型端侧部署的四个核心环节在正式进入QNN之前你得先在大脑里搭一个框架一个大模型要从训练好的权重文件变成在高通NPU上运行的推理任务到底经历了什么。我给这个过程分了四个阶段。第一步是模型导出把PyTorch、TensorFlow训练好的模型转成中间表示最常见的是ONNX格式。第二步是模型转换与量化用高通提供的工具把ONNX模型转成QNN格式同时完成FP32到INT8或INT16的量化这个环节直接影响性能和精度。第三步是模型编译与优化QNN编译器会根据目标硬件NPU、GPU、CPU对计算图做算子融合、内存规划、指令调度等优化。第四步是运行时推理应用通过QNN Runtime加载编译后的模型在端侧完成推理。这四个环节环环相扣哪一步出了问题后面全白搭。我在实际项目中经常遇到的情况是模型导出没问题转换也没报错但一上板跑就出错或精度崩溃最后排查半天发现是量化环节的数据校准没做好。所以后面我会重点展开量化和转换这两个环节这是大模型端侧部署的重灾区。2.2 为什么偏偏是高通QNN而不是SNPE或ONNX Runtime很多刚接触高通平台的人会有个疑问不是有SNPE吗不是可以直接上ONNX Runtime吗为什么非要学习QNN先说说SNPE和QNN的关系。SNPE是高通上一代神经网络推理框架在骁龙845到骁龙8 Gen 1那个时代是主流对老模型的支持也相对成熟。但从骁龙8 Gen 1之后高通把NPU架构升级到了Hexagon V73/V75等新代际SNPE对新一代NPU特性的利用已经跟不上了。QNN是高通战略上的接班产品针对新架构重新设计了整个runtime和编译器对Transformer类模型也就是大模型的主流结构的支持也更完善。再说ONNX Runtime。它本身是一个跨平台的推理框架确实也能跑在高通设备上但默认走的是CPU或OpenCL的GPU路径没法充分利用Hexagon DSP/NPU的算力。要知道高通芯片上NPU的推理能效比CPU高一个数量级你想在端侧跑大模型还想控制功耗和发热就必须让模型跑到NPU上而QNN就是进入NPU的唯一正式通道。说白了QNN不是可选项而是必选项。3. QNN整体架构与核心组件拆解3.1 从SDK目录到运行时QNN的组件全景如果你下载过高通QNN SDK解压后会看到一堆目录lib、bin、include、docs、examples等等。刚开始看会有点懵这里我帮你把QNN的关键组件梳理成一张“地图”。QNN整个框架分为三层。最上层是前端工具链包括模型转换器qnn-onnx-converter、qnn-tflite-converter等、模型量化工具、模型调试工具。中间层是后端运行时也就是QNN Runtime的核心库负责加载模型、调度执行、管理内存。最底层是HTP后端Hexagon Tensor Processor这是真正在NPU上执行算子计算的部件通过DSP驱动和上层交互。这里特别要提一个概念QNN的后端Backend。QNN不是只有NPU一个后端它支持CPU后端、GPU后端和HTPNPU后端。当你用QNN工具链转换模型时可以选择为不同后端编译模型。同一个DLC或.bin文件在不同后端的执行效率和能力完全不同。大模型部署最理想的情况是计算图大部分算子都落到HTP上执行只有少量控制流或复杂逻辑留在CPU上兜底。3.2 QNN的三种后端选型CPU、GPU、HTPNPU怎么选在QNN框架里后端选型直接决定了你能跑什么模型、性能上限在哪。这里我做个对比表格帮你快速理解差异后端类型计算核心优势劣势适用场景CPU Backend高通Kryo/Silver核心算子支持最全精度最容易保持算力低功耗高模型验证、算子兜底、小模型GPU BackendAdreno GPU并行能力强浮点性能好内存带宽受限Transformer优化一般图像类模型、部分卷积网络HTP BackendHexagon DSP能效比极高INT8/INT16优化强算子支持有限对量化敏感大模型、Transformer、端侧主力推理从我实测经验来看大模型尤其是LLM想跑出可用性能HTP后端几乎是唯一选择。原因是LLM推理的瓶颈在内存带宽和矩阵乘法上而Hexagon DSP对这两种负载都做了专门优化。GPU虽然浮点算力强但在运行自回归生成任务时小batch的矩阵乘很难吃满GPU并行度反而被内存带宽卡死。3.3 HTP后端与NPU硬件架构的对应关系聊完软件层我们再往深挖一层QNN的HTP后端到底是怎么跟NPU硬件交互的。高通Hexagon DSP从V66开始逐步演进到了骁龙8 Gen 3上的Hexagon V75NPU部分已经是一个完整的AI计算集群。从硬件架构上看高通车载芯片或者旗舰手机SoC里的NPU大致由这几个部分组成标量处理单元Scalar Unit、向量处理单元Vector Unit也就是Hexagon Vector eXtensionsHVX、张量处理单元Tensor Unit用于矩阵乘和卷积加速、大型共享内存LPMLarge Pool Memory。这些单元协同工作分别擅长处理不同粒度的计算QNN的HTP后端本质上是把计算图调度到这些硬件单元上。理解了这层硬件关系你就能明白为什么QNN对算子的支持“挑三拣四”。有些算子比如LayerNorm在标量单元上执行效率不错但放到向量单元上反而慢有些算子比如自定义激活函数如果没做硬件映射编译器只能把它切碎成多个基础算子产生大量内存搬移性能直接崩掉。所以在做模型转换前先查一下目标算子有没有HTP优化实现能省掉大量后面调优的时间。4. 大模型转换的实操链路从PyTorch到QNN的完整流程4.1 环境准备与工具链版本选择实操环节开始。首先得把环境搞定。QNN SDK的版本选择是个容易被忽视但影响巨大的细节。不同版本对框架版本、Python版本、操作系统都有要求。我自己踩过的一个坑是用新版本SDK的Python API去加载旧版本转换出来的模型提示各种不兼容的报错最后重装环境才解决。当前比较稳妥的做法是到高通官网下载最新的QNN SDK解压后严格按文档里的Python版本来创建虚拟环境不要图省事直接用系统Python。同时建议把ONNX、PyTorch、NumPy的版本锁定在SDK文档推荐的范围内版本漂移是转换报错的头号来源。环境准备好之后还要检查一下你的目标设备是否支持QNN。目前QNN主要支持骁龙8系列、8cx系列以及高通SA系列车载芯片比如SA8295P、SA8650P这些平台。如果你手头是骁龙6系或者更老的平台建议先查一下是否在支持列表里避免白忙一场。4.2 模型导出ONNX导出时的关键设置与检查点大模型转换的第一步是把训练框架的模型导出成ONNX。这一步看似简单其实有很多细节。首先导出前要把模型切到eval模式关闭dropout、batch normalization的training状态。其次要固定输入尺寸。如果你导出时用了动态轴dynamic_axes转成QNN后可能会因为动态shape支持不佳而报错。对于大模型来说最省心的做法是固定序列长度和batch size比如固定batch1, seq_len2048这样模型转换和内存规划都能做到最优。导出之后一定要用onnx.checker校验一遍模型同时用ONNX Runtime先跑一次输出记录一组参考输出值。这组输出值在后续量化精度验证时至关重要。我习惯把参考输出保存成.npy文件和模型放同一个目录这样后面做精度对比时随取随用。import torch import torch.nn as nn # 以LLaMA-like模型为例 model YourModel() model.eval() # 固定输入维度batch1, seq_len2048, hidden_size4096 dummy_input torch.randn(1, 2048, 4096, dtypetorch.float32) torch.onnx.export( model, dummy_input, your_model.onnx, opset_version17, input_names[input_ids], output_names[logits], dynamic_axesNone, # 固定shape不建议使用dynamic axes ) import onnx onnx.checker.check_model(your_model.onnx)4.3 QNN模型转换qnn-onnx-converter的参数与用法ONNX模型准备好之后接下来用QNN SDK自带的转换工具把它转成.bin文件。在SDK的bin目录下找到qnn-onnx-converterWindows下是.exeLinux下是二进制文件运行方式如下python qnn-onnx-converter \ --input_network your_model.onnx \ --output_path your_model.qnn \ --input_list input_list.txt \ --quantization_override ./quant_overrides.json \ --backend htp这里的input_list.txt非常重要它指定了用于校准的输入数据路径。QNN转换工具会读取这些真实输入数据来统计激活值的分布范围从而确定量化的scale和zero point。校准数据通常从训练集或验证集中抽取一般256到512条就够用覆盖不同类型的数据分布即可。quantization_override是进阶参数用于指定特定层的量化类型。比如有些层对精度极度敏感如最后的lm_head你可以强制它保留高精度而其他层继续走INT8量化。这个参数的用法后面量化章节会详细说。4.4 部署与推理QNN Runtime的加载和执行模型转换完成后下一步是在目标设备上用QNN Runtime跑起来。QNN提供了Python API和C API两种方式大模型场景我推荐直接用Python API做快速验证生产环境再考虑C封装。这里给一个最简单的Python推理示例核心流程是创建context → 加载模型 → 设置输入tensor → 执行推理 → 获取输出import qnn.python.QnnContext as QnnContext import qnn.python.QnnModel as QnnModel context QnnContext.QnnContext() context.init() model QnnModel.QnnModel() model.init(context, ./your_model.qnn) # 设置输入 model.set_input_tensor(input_ids, input_data) model.set_output_tensor(logits, output_data) # 执行推理 model.execute()如果你是在Linux设备上做A2AApplication to Application集成也可以直接调用libQnnHtp.soHTP后端和libQnnSystem.so系统管理库通过C API完成推理。不管用哪种方式核心逻辑都是**QNN模型加载 → 输入输出tensor绑定 → 执行 → 取回结果。**掌握了这个骨架后面做性能优化才有抓手。5. 大模型量化的关键细节与精度保持技巧5.1 量化不仅是“压体积”更是性能的胜负手很多人对量化有个误解觉得它就是把FP32的权重存成INT8减小模型体积。这么说只对了一半。在QNN的HTP后端上量化最核心的价值是让计算走硬件加速单元定点运算从而把NPU的算力真正用起来。Hexagon DSP对INT88位定点和INT1616位定点的吞吐量远超FP32尤其是矩阵乘和卷积这类算子定点化之后性能可以翻数倍。但代价也很明显精度损失。对于大模型来说激活值分布范围广、长尾明显简单粗暴的per-tensor量化往往让精度崩得厉害。我实测过一个7B模型Per-tensor量化后困惑度PPL暴涨30%以上根本没法用改成Per-channel权重量化和Per-token激活量化后PPL损失控制在3%以内这才算能接受。5.2 校准数据集的选择与生成策略量化归根结底是一个“找范围”的过程。校准数据集决定了你统计出来的激活值范围是否合理这一步做不好后面所有算子都是“盲人摸象”。我的经验是校准数据不能太少也不宜太多。太少统计出来的min/max不能反映真实分布太多校准时间长得难以忍受。256-512条比较合适但前提是数据要有足够的多样性。比如做LLM量化最好从不同主题、不同风格的文本中切出等长片段确保覆盖各种token组合。还有一个容易被忽略的细节校准数据的预处理要和推理时保持一致。比如生成LLM输入时有special token的填充、mask的设置校准数据也要用同样的方式生成否则输入分布不匹配量化参数自然就不准。5.3 混合精度量化哪些层必须“开小灶”在大模型量化里一个被验证过无数次的经验是不是所有层都能一视同仁地量化到INT8。和传统CNN不同LLM中有几个“精度敏感区”。首当其冲的是Embedding层和最后的输出层lm_head。Embedding层直接决定输入token的语义表示量化误差会被逐层放大lm_head的输出直接和生成质量挂钩Softmax前的logits出现偏差采样结果就完全不一样。这两层的权重我一般都保留FP16或INT16精度。其次是Attention模块中的QKQuery-Key矩阵乘。QK乘积决定了注意力分数的分布而这个分布往往对量化异常敏感。很多研究都指出注意力分数的大动态范围会让per-tensor量化直接失效。我的做法是对Q和K的乘积累加做INT16量化或者用per-token激活量化避免精度崩掉。实际操作中我通常会用quantization_override文件指定这些关键层的量化策略。这里给个示例片段展示怎么对特定层做INT16量化{ model_quantization: { overrides: [ { op_name: /model/layers/0/self_attn/q_proj, quantization: { weight: {dtype: int16}, activation: {dtype: int16} } }, { op_name: /lm_head, quantization: { weight: {dtype: int16}, activation: {dtype: int16} } } ] } }5.4 量化后的精度验证流程量化完了不能直接扔到设备上跑先做精度验证。我的标准流程是先用同一批测试输入分别用ONNX RuntimeFP32和QNN Runtime量化模型跑一遍输出计算余弦相似度或绝对误差。对于LLM更好用的指标是直接比对生成文本的困惑度差异。这里要特别注意QNN在CPU后端的输出和HTP后端的输出可能有细微差异因为CPU上可能走了不同的算子实现。所以精度验证一定要以最终部署的后端为准。我经常看到有人拿CPU后端的结果说精度达标结果一上NPU就露馅白白浪费排查时间。6. 大模型在QNN上的内存优化与性能调优6.1 内存布局对LLM推理的影响大模型端侧推理最大的敌人不是算力而是内存。一个7B模型光权重就是7GBFP16即使量化成INT8也要3.5GB左右对于车规芯片或者手机来说都不小。更关键的是运行时的中间激活值、KV-cache占用的内存往往被忽略但实际一跑起来内存说爆就爆。在QNN里内存管理主要通过Tensor属性Tensor Attribute和内存策略控制。默认情况下QNN会为每个tensor单独分配内存这种方式简单但浪费严重。优化思路是让QNN对tensor做内存复用比如在QnnContext创建时指定tensor_memory_strategy为reuse让生命周期不重叠的tensor共享同一块内存。对于Transformer结构来说每层的中间激活值生命周期很短内存复用后的峰值占用可以降低30%-50%。6.2 KV-Cache优化从朴素实现到GQA聊大模型推理绕不开KV-Cache。自回归生成时每生成一个token都要重新计算所有历史token的Key和Value如果不做缓存计算复杂度随序列长度平方级增长根本跑不了长文本。在高通平台上做KV-Cache优化首先要确认模型结构是否支持GQAGrouped Query Attention。GQA是LLaMA-2 70B、Mistral等模型采用的一种多头注意力变体它让多个查询头共享一组Key/Value头大幅减少KV-Cache的占用。实测下来从MHA换成GQAKV-Cache内存可以减少一半以上性能提升也很明显。如果你的模型还不是GQA结构建议在导出前先改造一下。其次是要充分利用INT8量化的KV-Cache。QNN HTP后端对INT8支持成熟KV-Cache量化后精度损失在可接受范围内但内存占用直接减半。这是一个性价比极高的优化点。6.3 性能剖析工具QNN Profiler的使用心得性能调优没有数据支撑就是瞎忙活。QNN SDK自带的qnn-profile-viewer和qnn-perf工具可以帮助我们定位热点算子。我用这套工具的常规流程是先在主机端用qnn-profile-viewer分析模型的静态计算图查看每个算子的耗时预估和内存占用然后到目标设备上用qnn-perf跑一次真实推理得到每个算子的实际执行时间。对比两张表就能迅速找出“预估很快但实际很慢”的算子这些往往是数据搬运、内存访问模式的瓶颈所在。印象最深的一次调优经历是一个LayerNorm算子占了整个推理时间的20%以上但我从计算量看它明明不应该这么慢。后来用qnn-perf发现问题出在输入tensor的内存布局不连续导致NPU在读取数据时要频繁做地址转换。把内存布局调整为连续之后这个算子的耗时直接降了一半还多。很多性能问题不是“算得慢”而是“搬得慢”。6.4 推理参数调优batch size与temperature的配合最后一个性能维度是推理参数。端侧大模型应用里batch_size通常只能设为1因为设备内存有限同时交互场景也很难做到多用户并发。但在一些非流式任务比如批量文本分类中适当增大batch size反而能提升吞吐量。另外temperature、top_k这些采样参数虽然不直接影响性能但会影响能否用贪心解码来替代采样解码。如果允许贪心解码推理逻辑可以简化为纯argmax很多分支判断可以省掉NPU计算图更规整执行效率更高。所以产品设计上如果能接受确定性输出尽量使用贪心策略性能和复现性都更好。7. 踩坑实录与高频问题的排查方法7.1 算子不支持先查HTP支持列表在QNN上部署大模型遇到的第一类高频问题就是“算子不支持”。报错信息通常长这样Unsupported operator: xxx。遇到这个我的排查顺序是先看docs目录下的算子支持列表确认该算子是否在HTP后端支持范围内。如果在检查算子的输入维度和dtype是否匹配如果不在考虑两种替代方案一是把该算子在onnx模型中替换为等价子图比如用SplitMulAdd实现某些自定义归一化二是把该算子切成CPU算子通过QNN的QnnContext异构执行让NPU和CPU协同工作。对于大模型高频出现的“问题算子”集中在LayerNorm、Softmax、GeLU这些基础组件上。好在QNN新版本对Transformer类的算子族支持已经很完整大部分情况可以通过昇算子版本解决。7.2 精度不对按这三步逐个排除精度问题也是高频问题。我的排查路径是老三步第一步确认FP32输出是否一致用ONNX Runtime和QNN CPU后端跑同一输入对比如果这里就不一致那是转换工具的问题和量化无关第二步确认INT8量化模型的输出误差看误差是否集中在量化敏感的层第三步逐层对比中间激活值定位是哪一层开始出现明显偏差。实际排查中我发现很多“精度问题”其实是模型在导出时算子融合导致的数值顺序变化引起的微小误差在FP32下就有但不明显量化后被放大。遇到这种情况可以考虑在onnx模型中显式关闭某些融合优化。7.3 运行时崩溃盯紧内存分配与驱动版本最后说一个最让人头大的问题模型转换成功、量化精度也正常但一跑就崩报内存错误或驱动错误。这类问题九成出在模型转换时指定的tensor shape与实际推理输入不一致或者QNN SDK版本与设备固件驱动版本不匹配。我的建议是先在系统日志里查驱动层报错信息然后确认设备上的HTP固件版本和SDK要求的版本是否一致。高通平台的驱动更新很快老固件跑新SDK经常出问题升级固件能解决很多莫名其妙的崩溃。这里整理一个我常用的问题速查表现象可能原因排查方法转换时报“Unsupported operator”算子不在HTP支持列表查算子列表替换或切CPU执行运行时内存暴涨动态shape导致内存规划不足固定输入shape调整内存复用策略推理结果全为0输入tensor未正确绑定检查输入名字和维度是否匹配崩溃但无明显错误HTP固件与SDK版本不符升级设备固件到SDK要求版本首次推理极慢模型未做预热或未持久化编译用QNN的缓存机制保存编译结果8. 给新手的几条实操建议绕了这么多最后再聊几条实在的建议。第一不要一上来就追求“全模型上NPU”。大模型结构复杂有些算子切到CPU上执行整体效率反而更高。QNN支持CPU和HTP异构协同合理划分计算图才是最优解。对于新手建议先用CPU后端把整条推理链路跑通再逐步把算子迁移到HTP每迁移一批就验证一次精度和性能这样定位问题会容易很多。第二量化参数一定要做实验对比不要迷信默认配置。QNN的默认量化配置在一些简单模型上表现良好但大模型场景几乎必须手动调优。建议做一个量化方案网格实验权重量化Per-Tensor vs Per-Channel× 激活量化Per-Tensor vs Per-Token× 关键层精度INT8 vs INT16一次性对比出最优组合。第三善用高通官方社区和文档。QNN的迭代速度很快很多坑官方文档有说明社区里也有大量经验帖。遇到诡异问题时先搜官方文档的Release Notes往往能找到答案。第四建议从较小的模型入手做验证。直接拿7B模型做QNN适配模型转换耗时、调优复杂度都会放大。先拿一个1B左右的模型把QNN整个工具链跑通再迁移到大模型效率会高很多。我自己第一次实践QNN时就是用一个很小的BERT分类模型跑通了全流程后续做大模型部署时整体操作就从容很多。大模型在高通平台上的部署是一项系统性工程QNN作为核心基础框架值得花时间吃透。这篇文章分享的内容都是我在实际项目中验证过的方法和经验希望能给你提供一个可靠的起步路径。说到底工具链再复杂也敌不过把每一个环节的原理和细节都理解清楚。