
1. 为什么要在MTK平台上折腾Qwen2.5先说结论在MTK平台上部署Qwen2.5不是把PC端那套流程照搬过来就能跑通的事。我前后在MTK平台上部署过三四个不同规模的大模型踩过的坑比想象中多得多尤其是从模型转换到推理落地这一段每一步都有平台特有的约束。MTK平台和我们在服务器上用的GPU环境有本质区别。MTK的芯片架构以ARM为核心NPU的算力调度、内存带宽、算子支持都和桌面级GPU完全不是一回事。Qwen2.5作为当前开源社区里综合表现相当均衡的大模型系列从0.5B到72B有多个尺寸可选但真正适合MTK平台部署的主要集中在0.5B、1.5B和3B这几个规格。7B以上的模型在MTK平台上跑推理除非你有非常明确的性能优化方案否则延迟和内存占用会让你怀疑人生。这篇文章面向的是已经在做端侧AI部署、或者准备在MTK平台上落地大模型推理的开发者。不管你是刚接触MTK平台的新手还是已经用过其他平台想迁移过来的老手我都会把从模型转换到推理部署的完整链路拆开讲清楚。核心关键词就四个MTK、Qwen2.5、模型转换、推理部署。我会重点讲清楚每一步为什么这么做、参数怎么选、哪些地方容易翻车。需要提前说明的是MTK平台的大模型部署工具链更新比较快不同版本的NeuroPilot SDK在API和算子支持上会有差异。我下面讲的内容基于我实际用过的几个版本你在操作时最好先确认自己手上的SDK版本避免因为版本不匹配导致一些莫名其妙的报错。2. 部署前的整体思路与方案选型2.1 为什么选Qwen2.5而不是其他模型Qwen2.5系列在端侧部署上有几个天然优势。第一它的tokenizer设计对中文和多语言支持很好这在端侧场景里很关键因为很多端侧应用需要处理中文输入。第二Qwen2.5的模型结构相对规整没有太多花哨的自定义算子这对MTK平台的算子映射来说是个好消息。第三Qwen2.5提供了多种尺寸的预训练模型和指令微调模型你可以根据目标设备的算力灵活选择。我对比过Qwen2.5-1.5B-Instruct和几个同量级的其他开源模型在MTK平台上的转换成功率明显更高。有些模型用了比较特殊的attention实现或者位置编码方式转换到MTK的NPU上会直接报算子不支持。Qwen2.5在这方面踩雷的概率低很多。2.2 MTK平台部署大模型的核心约束在MTK平台上部署大模型你首先要搞清楚三个硬约束内存带宽。MTK平台的NPU通常和CPU、GPU共享内存带宽大模型推理时权重加载会占用大量带宽。这就是为什么小尺寸模型在端侧更有优势——不是算力不够是带宽喂不饱。算子支持范围。MTK的NPU对算子的支持是有限集合不是所有PyTorch算子都能直接映射。Qwen2.5里用到的RMSNorm、RoPE、SwiGLU这些在较新的NeuroPilot版本里都有对应实现但如果你用的是老版本SDK可能需要自己做算子替换或者用CPU fallback。量化精度。端侧部署几乎必然要做量化MTK平台对INT8和INT4的支持比较成熟。但量化会带来精度损失Qwen2.5在INT4量化下1.5B模型的输出质量下降还在可接受范围内0.5B模型就有点勉强了。2.3 整体部署链路概览整个部署流程可以拆成四个阶段模型准备与导出、模型转换、推理引擎集成、性能调优。每个阶段都有明确的输入和输出我习惯把它们串成一条流水线来管理。模型准备阶段你需要从HuggingFace或者ModelScope拉取Qwen2.5的原始权重然后做必要的结构裁剪和配置调整。导出阶段通常是把PyTorch模型转成ONNX格式这一步是为了后续转换工具能识别。模型转换阶段是用MTK提供的转换工具把ONNX或者PyTorch模型转成MTK NPU能执行的格式。推理集成阶段是把转换后的模型嵌入到你的Android或者Linux应用里调通推理接口。性能调优阶段则是根据实际表现做量化策略调整、内存优化和算子替换。注意MTK平台的模型转换工具对ONNX的opset版本有要求我实测opset 14和opset 17比较稳太新的版本反而容易出问题。3. 模型转换全流程拆解3.1 环境准备与工具链安装在开始转换之前你需要把MTK的NeuroPilot SDK装好。这个SDK里包含了模型转换工具、推理运行时库和相关的Python包。我建议在Ubuntu 20.04或者22.04上做转换Windows环境下有些工具的支持不完整。安装步骤大致如下# 创建独立的Python环境避免依赖冲突 python3 -m venv mtk_env source mtk_env/bin/activate # 安装NeuroPilot SDK的Python包 pip install neuropilot-sdk # 安装模型转换工具 pip install mtk-model-converter # 验证安装 mtk-converter --version装完之后你还需要确认NPU的驱动版本和SDK版本匹配。我遇到过好几次转换工具能跑但推理时报错的情况最后发现是驱动版本太老。3.2 Qwen2.5模型导出为ONNX从HuggingFace拉取Qwen2.5-1.5B-Instruct的权重后第一步是导出ONNX。这里有个关键点Qwen2.5的attention实现里用了动态shape导出ONNX时需要固定序列长度否则后续转换工具处理不了。我通常会把序列长度固定为128或者256具体取决于你的应用场景。如果是对话场景128够用了如果是文档摘要可能需要512。import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_name Qwen/Qwen2.5-1.5B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained(model_name, trust_remote_codeTrue) # 设置为推理模式 model.eval() # 构造示例输入 dummy_input tokenizer(你好, return_tensorspt) # 导出ONNX torch.onnx.export( model, (dummy_input[input_ids], dummy_input[attention_mask]), qwen2.5_1.5b.onnx, input_names[input_ids, attention_mask], output_names[logits], dynamic_axes{ input_ids: {0: batch, 1: sequence}, attention_mask: {0: batch, 1: sequence}, logits: {0: batch, 1: sequence} }, opset_version14 )导出过程中最常见的报错是算子不支持。Qwen2.5用到的RotaryEmbedding在某些transformers版本里导出会出问题。如果遇到这种情况可以尝试升级transformers到最新版或者手动替换attention实现。3.3 ONNX到MTK NPU格式的转换这一步是整个流程里最关键的。MTK的转换工具叫mtk-converter它会把ONNX模型转成.mtknn格式这个格式才能被NPU直接加载执行。转换命令的基本结构mtk-converter \ --input qwen2.5_1.5b.onnx \ --output qwen2.5_1.5b.mtknn \ --target npu \ --quantize int8 \ --calibration-data calibration_data/ \ --input-shape 1,128 \ --input-shape 1,128这里有几个参数需要重点解释--quantize int8指定量化精度。INT8是端侧部署的标配精度损失可控性能提升明显。如果你的设备支持INT4也可以试试--quantize int4但Qwen2.5-1.5B在INT4下输出质量下降比较明显我一般不建议。--calibration-data是量化校准数据集。这个非常重要校准数据的质量直接决定量化后的精度。我通常从训练集或者实际业务数据里抽200-500条样本做校准。校准数据要覆盖你的实际使用场景比如你做的是客服对话校准数据就应该是客服对话的语料。--input-shape指定输入张量的形状。Qwen2.5有两个输入input_ids和attention_mask所以需要指定两次。形状要和ONNX导出时一致。转换过程中如果报算子不支持转换工具会给出具体的算子名称。常见的处理方式有三种一是升级SDK版本新版本通常会增加算子支持二是用CPU fallback把不支持的算子放到CPU上执行但会影响性能三是手动替换算子实现这个工作量比较大适合有经验的开发者。3.4 转换后的模型验证转换完成后不要急着集成到应用里先用MTK提供的验证工具跑一遍确认模型能正常加载和推理。mtk-validator \ --model qwen2.5_1.5b.mtknn \ --input test_input.bin \ --output test_output.bin \ --compare-with onnx_output.bin验证工具会对比MTK NPU的输出和原始ONNX的输出给出精度偏差。如果偏差在可接受范围内通常余弦相似度大于0.99说明转换成功。如果偏差很大大概率是量化校准出了问题需要重新调整校准数据或者换用量化策略。实操心得我习惯在转换前先把原始ONNX模型在CPU上跑一遍保存输出作为基准。这样验证的时候有明确的对比目标不用凭感觉判断。4. 推理引擎集成与实操4.1 MTK推理运行时接口调用MTK平台提供了C和Java两套推理接口。如果你的应用是Android原生开发用Java接口更方便如果是JNI层或者Linux环境用C接口性能更好。C接口的核心调用流程#include mtk_npu_runtime.h // 初始化运行时 MtkNpuRuntime runtime; runtime.init(); // 加载模型 MtkModel model runtime.loadModel(qwen2.5_1.5b.mtknn); // 准备输入 std::vectorint32_t input_ids {151644, 872, 198, ...}; std::vectorint32_t attention_mask(input_ids.size(), 1); // 创建输入张量 MtkTensor input_tensor_ids model.createInputTensor(input_ids, {1, input_ids.size()}); MtkTensor input_tensor_mask model.createInputTensor(attention_mask, {1, attention_mask.size()}); // 填充数据 input_tensor_ids.copyFrom(input_ids.data()); input_tensor_mask.copyFrom(attention_mask.data()); // 执行推理 std::vectorMtkTensor inputs {input_tensor_ids, input_tensor_mask}; MtkTensor output model.run(inputs); // 获取输出 std::vectorfloat logits(output.elementCount()); output.copyTo(logits.data());这段代码看起来简单但实际集成时有几个坑。第一输入张量的形状必须和转换时指定的完全一致否则运行时会直接报错。第二Qwen2.5是自回归模型每次生成一个token都需要重新跑一次推理所以你需要自己实现KV Cache的管理否则性能会非常差。4.2 KV Cache的实现要点KV Cache是大模型推理加速的核心机制。简单说就是每次生成新token时不需要重新计算前面所有token的Key和Value而是把之前算好的缓存起来复用。在MTK平台上实现KV Cache你需要把模型拆成两个部分prefill阶段和decode阶段。Prefill阶段处理完整的输入序列生成初始的KV CacheDecode阶段每次只处理一个新token从Cache里读取历史信息。MTK的转换工具支持把模型导出为带KV Cache的格式但需要你在导出ONNX时就做好相应的处理。具体做法是在模型里显式定义past_key_values和present_key_values的输入输出。# 导出带KV Cache的ONNX torch.onnx.export( model, (input_ids, attention_mask, past_key_values), qwen2.5_1.5b_kv.onnx, input_names[input_ids, attention_mask, past_key_values], output_names[logits, present_key_values], dynamic_axes{...}, opset_version14 )KV Cache的管理逻辑需要你自己在推理代码里实现。我通常用一个环形缓冲区来存储KV Cache每个decode step更新一次。缓冲区的大小取决于你的最大序列长度和模型层数。4.3 推理性能实测与调优我在MTK Dimensity 9300平台上实测过Qwen2.5-1.5B-Instruct的推理性能。INT8量化后prefill阶段128 token输入耗时约180msdecode阶段每个token约25ms。这个性能对于端侧对话应用来说基本可用但还有优化空间。优化方向主要有三个算子融合。MTK的转换工具支持自动算子融合把连续的多个算子合并成一个减少内存访问次数。你可以在转换时加上--enable-fusion参数开启。内存复用。推理过程中会频繁分配和释放内存用内存池来管理可以显著减少开销。MTK的运行时提供了内存池接口建议在初始化时就预分配好。线程绑定。MTK平台是大小核架构把推理线程绑定到大核上可以避免被小核拖慢。用taskset或者sched_setaffinity来设置CPU亲和性。# 把推理进程绑定到大核 taskset -c 4-7 ./your_inference_app注意线程绑定不是万能的如果大核被其他任务占满反而会导致推理延迟增加。建议在实际场景下测试后再决定是否绑定。5. 常见问题与排查技巧实录5.1 模型转换阶段的典型报错报错一Unsupported operator: aten::xxx这是最常见的报错。MTK的转换工具不支持某些PyTorch算子。解决思路是先用onnx-simplifier简化ONNX模型把一些冗余算子消掉。如果还是不行就需要手动替换算子实现。报错二Calibration failed: insufficient data量化校准失败通常是校准数据太少或者分布太单一。我一般会准备至少200条校准样本覆盖不同的输入长度和内容类型。报错三Shape mismatch in input tensor输入形状不匹配。检查ONNX导出时的dynamic_axes设置和转换时的input-shape参数是否一致。5.2 推理阶段的性能问题问题一首次推理特别慢首次推理需要加载模型权重到NPU内存耗时较长是正常的。但如果后续推理也很慢可能是模型没有正确驻留在NPU内存里。检查一下是否每次推理都重新加载了模型。问题二内存占用过高Qwen2.5-1.5B的INT8量化模型大约占1.5GB内存加上KV Cache和运行时开销总共需要2GB左右。如果设备内存紧张可以考虑用INT4量化但精度会下降。问题三输出乱码或者重复这通常是量化精度损失导致的。尝试增加校准数据量或者换用更保守的量化策略。如果问题依旧可能是tokenizer的配置有问题检查一下tokenizer的special tokens设置。5.3 常见问题速查表问题现象可能原因排查方向解决方案转换报算子不支持SDK版本过旧查看报错算子名称升级SDK或替换算子量化后精度骤降校准数据不足检查校准集大小和分布增加校准样本至200条以上推理延迟高线程未绑定大核检查CPU亲和性设置用taskset绑定大核输出重复量化精度损失对比FP32和INT8输出换INT8或增加校准数据模型加载失败驱动版本不匹配检查驱动和SDK版本升级驱动到匹配版本内存溢出KV Cache过大检查最大序列长度设置减小序列长度或优化Cache5.4 独家避坑技巧第一个技巧在转换之前先用ONNX Runtime在PC上跑一遍完整的推理流程确认模型本身没有问题。这样可以把模型问题和转换问题分开排查。第二个技巧校准数据不要只用一种类型的文本。我试过只用新闻语料做校准结果在对话场景下输出质量很差。后来混合了对话、新闻、代码三种语料效果明显改善。第三个技巧如果MTK平台上的推理结果和PC上差异很大先检查attention mask的处理逻辑。MTK的NPU对attention mask的格式有特定要求格式不对会导致attention计算错误。6. 端侧部署的扩展思考6.1 多模型协同的部署策略在实际产品里你往往不会只部署一个大模型。可能是Qwen2.5-0.5B做意图识别Qwen2.5-1.5B做对话生成再加一个小的分类模型做路由。这种多模型协同的场景下内存管理就变得很关键。我的做法是把所有模型放在同一个运行时实例里共享内存池。MTK的运行时支持多模型加载但需要注意模型之间的内存隔离避免一个模型的推理影响另一个模型。6.2 模型更新与热替换端侧模型更新是个麻烦事。用户不可能每次都重新安装整个应用。我的方案是把模型文件放在独立的目录里应用启动时检查版本号有更新就下载新的模型文件然后重新初始化推理引擎。MTK的运行时支持动态卸载和加载模型但卸载时一定要确保没有正在执行的推理任务否则会导致崩溃。6.3 从MTK迁移到其他平台的注意事项如果你后续需要把模型迁移到其他端侧平台比如高通的SNPE或者华为的CANN有几点需要注意。第一量化策略可能不同MTK的INT8量化方案不一定能直接复用。第二算子支持范围不同某些在MTK上能跑的算子在其他平台上可能需要替换。第三KV Cache的实现方式可能有差异需要重新适配。我个人的经验是模型转换和推理集成的代码尽量做成平台无关的抽象层把平台相关的部分封装成独立的模块。这样迁移的时候只需要替换平台适配层上层逻辑不用动。6.4 实际产品中的性能取舍最后聊一个实际问题端侧大模型的性能取舍。你不可能在端侧做到和云端一样的推理速度所以必须做取舍。我的建议是优先保证首token延迟因为用户对首token的感知最明显。Decode阶段的速度可以适当放宽只要整体对话体验流畅就行。另外不要盲目追求大模型。Qwen2.5-0.5B在很多场景下已经够用了尤其是做意图识别、简单问答这类任务。1.5B适合需要一定推理能力的场景3B以上就要慎重考虑了除非你的设备有足够的内存和算力。我在实际项目里用过Qwen2.5-0.5B做本地意图分类准确率能达到90%以上推理延迟只有几毫秒。这种场景下用大模型反而是浪费。选模型的时候先明确你的任务复杂度再决定模型尺寸不要一上来就选最大的。