1. 为什么要在手机上折腾大模型推理把 Qwen 这类模型塞进手机里跑最早我是被一个离线场景逼出来的。当时做一个户外巡检的小工具现场经常没有可用的网络但需要实时把巡检记录做结构化抽取和摘要。云端 API 走不通笔记本又太重唯一随身带着的算力就是那台骁龙 8 Gen 2 的手机。于是就有了这条链路先在服务器上把 Qwen 做 LoRA 微调再量化导出成 ONNX最后丢给骁龙 Hexagon NPU 通过 QNN 执行。这条路走通之后我发现它解决的不只是没网这一个问题。端侧推理真正的价值在于三点数据不出设备、响应延迟可控、长期没有调用成本。适合谁来参考如果你手上有骁龙平台的设备手机、平板、开发板都算想跑一个 1B 到 7B 量级的 Qwen 做本地问答、文本抽取、简单 Agent那这套流程基本可以直接抄。如果你只是想体验一下大模型那还是先用现成的 App 更省事。需要先泼一盆冷水手机 NPU 不是缩小版的 GPU。它的强项是定点运算和低功耗常驻弱项是算子覆盖不全、内存带宽有限、工具链成熟度远不如 CUDA。所以整条链路的核心矛盾不是怎么把模型跑起来而是怎么把模型改造成 NPU 喜欢的样子。理解这一点后面所有的量化、算子替换、图切分才有意义。2. 整体方案设计与技术选型拆解2.1 为什么是 Qwen LoRA ONNX QNN 这条组合先说我为什么选 Qwen 而不是别的模型。Qwen 系列在中英双语上的表现比较均衡而且官方对量化和小尺寸版本的支持比较积极1.8B、4B 这些规格在端侧刚好卡在一个能力够用、显存吃得下的区间。更关键的是它的 tokenizer 和结构相对规整导出 ONNX 时踩的坑少。微调环节我选LoRA而不是全参微调理由很实际全参微调一个 4B 模型单卡 24G 都紧张而且产出的权重动辄十几 G端侧根本放不下。LoRA 只训练低秩旁路矩阵参数量通常只有原模型的百分之几训练快、显存省合并回主权重后推理时零额外开销。对于让模型学会某个垂直领域的输出格式这种需求LoRA 完全够用。导出环节选ONNX是看中它的中立性。PyTorch 权重没法直接喂给 QNN中间必须有一个标准化的图表示。ONNX 既是 PyTorch 导出的默认目标又能被 QNN 的转换工具链消费是这条链路上最省事的中间格式。最后落到QNNQualcomm Neural Processing SDK。骁龙平台访问 Hexagon NPU 的官方路径就是 QNN它提供了模型转换、图优化、后端选择CPU/GPU/DSP/NPU的一整套工具。虽然它的文档写得让人想摔键盘但它是目前唯一能真正把算子调度到 Hexagon 上的成熟方案。2.2 整条链路的分段与职责我把整个流程拆成四段每段有明确的输入输出这样出问题的时候能快速定位是哪一段的锅阶段输入输出主要工具关键风险微调基座 Qwen 领域数据LoRA 权重PEFT / LLaMA-Factory数据格式、过拟合合并导出基座 LoRAFP32 ONNXPyTorch / Optimum动态轴、算子版本量化FP32 ONNXINT8 ONNXonnxruntime 量化工具精度掉点、校准集部署INT8 ONNXQNN context binaryQNN SDK算子不支持、图切分这张表建议你打印出来贴在显示器边上。我踩过的坑里八成都能归到某一行的关键风险里。2.3 一个容易被忽略的前置判断你的设备到底有没有 NPU不是所有骁龙都带 Hexagon NPU也不是带了就能用。判断方法很直接查芯片型号。骁龙 8 系从 8 Gen 1 开始 NPU 能力比较完整7 系部分型号有6 系基本别指望。另外同一颗芯片在不同厂商的固件里NPU 的可用性也可能被限制这个只能实测。提示在动手之前先用设备跑一个官方的 QNN 示例比如图像分类 demo确认 NPU 后端能正常加载。这一步能帮你排除掉一半环境问题省下大量瞎折腾的时间。3. 微调阶段让 Qwen 学会你的活儿3.1 数据准备格式比数量重要LoRA 微调最容易被低估的就是数据。我见过太多人拿几千条脏数据去训结果模型学会了胡说八道。端侧模型参数量小对数据质量更敏感我的经验是500 到 2000 条高质量样本远胜 5 万条噪声数据。数据格式用标准的指令微调格式就行Qwen 官方推荐的是带 system/user/assistant 的对话结构。我一般会把它整理成 JSONL每行一条{conversations: [{from: user, value: 把这段巡检记录抽成JSON3号泵压力偏高温度正常}, {from: assistant, value: {\device\: \3号泵\, \pressure\: \high\, \temperature\: \normal\}}]}这里有个实操心得输出格式一定要在训练数据里高度一致。端侧模型没有云端那么强的泛化能力你希望它输出 JSON那训练集里每一条 assistant 回复都必须是严格合法的 JSON一个多余的空格都可能让它在推理时跑偏。3.2 LoRA 参数怎么定LoRA 的核心参数就三个rankr、alpha、dropout。我的默认配置是 r8、alpha16、dropout0.05这套组合在 1.8B 到 4B 的 Qwen 上比较稳。为什么 r 取 8 而不是更大rank 越大能拟合的能力越强但参数量也越大端侧合并后的模型体积会涨。对于学一个输出格式这种任务r8 足够如果你要注入比较复杂的领域知识可以提到 16 或 32但要相应增加数据量否则容易过拟合。alpha 一般取 r 的两倍这是社区里比较通行的经验值作用是缩放 LoRA 旁路的贡献。dropout 设小一点0.05 到 0.1 之间主要是防止小数据集上的过拟合。训练超参方面学习率我用 1e-4 到 2e-4epoch 控制在 3 到 5 轮。判断该不该停的一个土办法盯着验证集的 loss一旦连续两轮不降反升立刻停别恋战。3.3 合并权重时的坑训练完得到的是 LoRA adapter推理前要合并回基座。这一步用 PEFT 的merge_and_unload()就行。但这里有个坑合并时的精度。如果你在 FP16 下合并再转 FP32 导出可能会引入额外的数值误差。我的做法是合并时就用 FP32虽然慢一点但后面量化时精度基线更干净。合并完先别急着导出用几道测试题验证一下合并后的模型输出是否和合并前一致。我遇到过合并后输出乱码的情况最后发现是 tokenizer 配置没跟着走。这种问题在导出成 ONNX 之后极难排查一定要在 PyTorch 阶段就确认干净。4. 导出与量化把模型改造成 NPU 喜欢的样子4.1 导出 ONNX 的关键设置从 PyTorch 导出 ONNX我一般用 Optimum 或者直接torch.onnx.export。核心是几个参数torch.onnx.export( model, dummy_inputs, qwen_fp32.onnx, input_names[input_ids, attention_mask, position_ids], output_names[logits], dynamic_axes{ input_ids: {0: batch, 1: seq}, attention_mask: {0: batch, 1: seq}, position_ids: {0: batch, 1: seq}, }, opset_version17, do_constant_foldingTrue, )opset 版本建议 17 及以上低版本对 Transformer 里的一些算子支持不好。dynamic_axes 一定要设否则序列长度被写死端侧没法处理变长输入。导出后第一件事是用 onnxruntime 跑一遍和 PyTorch 的输出做数值对比。允许的误差范围FP32 下 logits 的最大绝对误差应该在 1e-4 以内。如果超了说明导出过程有问题别往下走。4.2 量化INT8 是端侧的甜点区端侧部署量化几乎是必选项。FP32 的 4B 模型光权重就 16G手机根本放不下INT8 直接砍到四分之一4G 左右勉强能进。再往下 INT4 体积更小但精度掉得厉害算子支持也差我一般不碰。量化方式我推荐静态量化static quantization而不是动态量化。静态量化需要一份校准数据集用它统计激活值的分布从而确定量化参数。校准集不用多100 到 300 条有代表性的样本就够但必须覆盖你实际推理时会遇到的输入分布。我吃过亏用通用语料校准结果在领域数据上精度崩了后来换成领域样本校准问题消失。用 onnxruntime 的量化工具大致是这样from onnxruntime.quantization import quantize_static, CalibrationDataReader quantize_static( model_inputqwen_fp32.onnx, model_outputqwen_int8.onnx, calibration_data_readerMyCalibReader(), quant_formatQuantFormat.QDQ, per_channelTrue, weight_typeQuantType.QInt8, )per_channelTrue很重要逐通道量化比逐张量量化精度好不少代价是模型稍微大一点点值得。QuantFormat.QDQ生成的图带 Quantize/Dequantize 节点QNN 转换时更容易识别。4.3 量化后的精度验证不能省量化完必须做精度对比。我的做法是准备一组固定的测试 prompt分别用 FP32 和 INT8 模型跑比较输出的困惑度或者直接看生成结果。经验阈值困惑度上升不超过 5%生成结果语义基本一致就算合格。如果掉点严重回去检查校准集或者对敏感层比如第一层和最后一层跳过量化。注意不要迷信量化无损这种说法。任何量化都有精度损失关键是损失是否在你的可接受范围内。端侧场景下用户对偶尔的小错误容忍度其实比你想的高但对完全不能用是零容忍。5. QNN 部署真正把算子压到 Hexagon 上5.1 环境搭建与模型转换QNN SDK 的安装这里不展开官方文档有。重点说模型转换。QNN 提供了qnn-onnx-converter工具把 ONNX 转成 QNN 的中间格式qnn-onnx-converter \ --input_network qwen_int8.onnx \ --output_path qwen_qnn.cpp \ --input_dim input_ids 1,128 \ --input_dim attention_mask 1,128 \ --input_dim position_ids 1,128 \ --quantization_overrides quant_overrides.json这里--input_dim要写死一个具体尺寸因为 QNN 的图编译需要静态 shape。端侧处理变长输入的做法是按最大长度编译短输入做 padding。这会浪费一点算力但换来的是图编译的稳定性。quant_overrides.json是量化参数的覆盖文件用来对齐 ONNX 量化时确定的 scale 和 zero_point。这一步如果对不齐精度会莫名其妙地掉。5.2 算子不支持怎么办图切分QNN 最让人头疼的就是算子覆盖。Qwen 里的一些算子比如特定的 attention 变体、RoPE 的实现方式可能不被 Hexagon 后端支持。这时候有两个选择改模型结构或者做图切分。改模型结构是治本但工作量大。图切分是治标把不支持的算子丢回 CPU 跑支持的留在 NPU。QNN 支持通过--op_package_config指定哪些算子走哪个后端。我的经验是attention 和 FFN 的矩阵乘尽量留在 NPULayerNorm、Softmax 这类如果 NPU 不支持就丢 CPU。虽然会引入 CPU-NPU 之间的数据搬运开销但总体还是比全 CPU 快。判断一个算子支不支持最直接的方法是转换时看日志。QNN 会明确告诉你哪些算子 fallback 到了 CPU。如果 fallback 的算子太多NPU 加速就名存实亡了这时候要重新评估方案。5.3 在设备上跑起来转换完成后用qnn-context-binary-generator生成 context binary这是最终部署到设备上的产物。然后在设备端用 QNN 的 runtime API 加载执行。Android 上一般通过 JNI 调用iOS 走不了这条路Hexagon 是骁龙的苹果设备用不了。实测下来一个 1.8B 的 Qwen INT8 模型在骁龙 8 Gen 2 上prefill 阶段处理输入大概能到几十 token/sdecode 阶段逐 token 生成会慢一些。这个速度做文本抽取、短问答是够用的做长文生成就比较勉强。6. 常见问题与排查实录6.1 问题速查表现象可能原因排查方向转换时报算子不支持算子版本或结构不兼容看日志定位算子考虑图切分或改结构推理结果乱码tokenizer 不一致确认端侧 tokenizer 与训练时完全一致精度大幅下降量化校准集不匹配换领域样本重新校准NPU 没被调用后端配置错误检查 context binary 的后端设置推理速度慢大量算子 fallback 到 CPU分析图切分比例内存溢出模型太大或序列太长降序列长度或换更小模型6.2 几个独家避坑技巧第一个tokenizer 一定要端侧对齐。我遇到过最诡异的问题就是输出乱码查了两天才发现是端侧用的 tokenizer 版本和训练时差了一个小版本词表映射错位。现在我的做法是把 tokenizer 的所有配置文件打包进部署产物端侧直接加载绝不依赖系统默认。第二个校准集要像真实输入。前面提过这里再强调一次。校准集决定了量化参数量化参数决定了精度。用通用语料校准领域模型等于让一个从没见过你数据的人去猜你的数据分布结果可想而知。第三个先跑通再优化。很多人一上来就想把整个 7B 模型塞进 NPU结果卡在算子不支持上出不来。我的建议是先用一个 0.5B 的小模型把整条链路跑通确认微调、导出、量化、部署每一环都 OK再换大模型。链路通了换模型只是参数调整的事。第四个保留 FP32 基线。任何时候都要有一个 FP32 的 PyTorch 版本作为精度参照。量化、转换、部署每一步都可能引入误差没有基线你根本不知道是哪一步出的问题。6.3 关于性能的一点现实预期最后说点实在的。手机 NPU 跑大模型别指望能和云端比。它的定位是在特定场景下提供可用的本地推理能力不是替代云端。我实测下来1.8B 模型做结构化抽取准确率能到可用水平4B 模型做摘要质量明显更好但速度慢一截。选模型的时候先明确你的场景对延迟和质量的容忍度再倒推该用多大的模型而不是反过来。这套流程我前后迭代了大概两个月中间推翻重来过一次。最大的体会是端侧部署的难点从来不在跑起来而在跑得对、跑得稳。工具链的坑、量化的坑、算子兼容的坑每一个都得亲自踩一遍才记得住。但一旦链路打通你会发现端侧推理能做的事情比想象中多尤其是那些对数据隐私和离线可用性有硬要求的场景这条路几乎是唯一解。
