MTK GAI Toolkit实战:Qwen2.5-1.5B端侧部署与INT8量化全流程
最近在折腾MTK GAI Toolkit把手头的Qwen2.5-1.5B模型从HuggingFace拉下来一路转换、量化再部署到天玑平台的端侧设备上整个过程踩了不少坑今天把完整流程拆开讲清楚。这篇更适合已经有模型部署基础、但还没跑通过端侧全链路的朋友如果你是第一次接触端侧AI也能照着步骤复现只是中间会解释得细一些。MTK GAI Toolkit是联发科面向生成式AI推出的端侧部署工具链核心作用是把HuggingFace等平台上的大模型转换成能在天玑平台NPU/APU上高效运行的格式。Qwen2.5-1.5B作为小尺寸对话模型正好是这种工具链最典型的验证对象参数量适中、硬件门槛不高、部署后还能保留对话能力。下面所有流程都是我自己实际跑过的不是单纯翻译官方文档。1. 项目背景与整体思路1.1 为什么选Qwen2.5-1.5B端侧部署模型选型第一条就是参数量。Qwen2.5-1.5B是1.5B参数规模FP16精度下权重大约3GBINT8量化后能压到800MB左右INT4量化后大概500MB以内。这个体量对手机和平板设备很友好8GB内存的设备跑起来不紧张后台驻留和发热也相对可控。另外Qwen2.5系列本身用的是标准Transformer decoder架构比起某些厂商的私有结构它更容易被工具链识别和转换。这意味着从HuggingFace拉下来之后不需要做大量模型结构适配导出ONNX时踩坑概率小很多。当然1.5B模型的生成能力和7B、14B没法比但它在端侧的价值体现在响应速度上。实测在工程机上INT8量化后单token生成延迟能做到几十毫秒级别用户体感是“边打字边回答”而不是盯着菊花转半天。做离线助手、文档摘要、智能输入法这类场景完全够用。1.2 MTK GAI Toolkit到底解决什么问题裸跑模型其实也能部署把Qwen2.5-1.5B换成ONNX Runtime或者TFLite再写个封装就能跑。但问题在于纯CPU推理的能效比太低手机SOC上最值钱的NPU/APU算力完全没利用上。MTK GAI Toolkit的核心价值就是把模型转换、量化、算子映射到天玑平台的硬件加速单元上让NPU参与计算。工具链的工作流可以理解成一条流水线原始PyTorch/Transformer模型 - ONNX - 量化校准 - MTK专用格式 - 端侧runtime加载。其中MTK专用格式不是简单的压缩包它包含了算子的硬件映射表、内存布局、甚至多核调度策略这些信息在标准ONNX里是没有的。我这次实战用的版本是2025年初更新的工具链CLI命令风格和传统交叉编译工具很像而且提供Python SDK可以在Python里完成大部分转换工作特别适合和HuggingFace仓库的脚本串在一起用。2. 环境准备与模型拉取2.1 环境依赖与工具链安装别看后面又是转换又是量化环境准备其实很简单。我建议直接新建一个Python 3.10的虚拟环境避免和系统Python混在一起。python -m venv mtk_venv source mtk_venv/bin/activate安装基础依赖时注意版本锁定别脑子一热全部装最新版。transformers、torch、onnx这三个版本太激进的话在导出阶段会出现奇怪的不兼容。pip install torch2.1.2 transformers4.43.2 optimum1.20.0 onnx1.16.0 \ huggingface_hub0.23.0 protobuf sentencepieceMTK GAI Toolkit的安装包目前优先支持Ubuntu 20.04/22.04和Windows 11Mac也可以装但部分算子映射验证功能不完整。安装命令大概是pip install mtk-gai-toolkit装完后验证一下版本同时检查CLI是否正常工作mtk_gai --version mtk_gai verbose如果CLI命令找不到多半是虚拟环境的bin目录没写进PATH官方安装包也会附带一个环境变量配置脚本手动source一下就行。2.2 从HuggingFace拉取Qwen2.5-1.5B模型HuggingFace上有好几个Qwen2.5-1.5B相关的仓库最常用的是Qwen/Qwen2.5-1.5B-Instruct带对话能力部署场景更实用。我惯用huggingface-cli下载因为速度比git clone快很多而且支持断点续传。pip install -U huggingface_hub[cli] export HF_ENDPOINThttps://hf-mirror.com huggingface-cli download Qwen/Qwen2.5-1.5B-Instruct \ --local-dir ./models/Qwen2.5-1.5B-Instruct \ --local-dir-use-symlinks False这里用HF_ENDPOINT指向镜像站是实操里比较省心的一步。如果你在网络条件好的环境下访问HuggingFace没有障碍可以忽略这个环境变量但设置镜像不会影响下载结果图个稳定。下载成功后目录里至少会有config.json、model.safetensors、tokenizer.json、tokenizer_config.json这几个关键文件。下载完别急着转格式先做个文件完整性检查LFS文件经常因为网络中断出现半截情况find models/Qwen2.5-1.5B-Instruct -type f -name *.safetensors -exec ls -lh {} \;safetensors文件一般3GB左右如果缺了就可以重新拉。确认没问题后再用Python快速验证模型能加载from transformers import AutoModelForCausalLM, AutoTokenizer model_dir ./models/Qwen2.5-1.5B-Instruct tok AutoTokenizer.from_pretrained(model_dir, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained(model_dir, trust_remote_codeTrue, torch_dtypeauto) print(model.config.hidden_size)能正常打印出hidden_size就说明模型文件是完整的这时候再进入转换环节。3. 模型转换与量化3.1 导出ONNX模型MTK工具链不吃原生PyTorch权重标准入口是ONNX。我用的是Optimum库导出的标准流程比手动改torch.onnx.export省很多力气。optimum-cli export onnx \ --model ./models/Qwen2.5-1.5B-Instruct \ --task text-generation-with-past \ --opset 14 \ ./models/Qwen2.5-1.5B-ONNX关键参数是--task text-generation-with-past它会把past_key_values缓存一起导出这样推理时不用每步重新计算历史attention速度差距非常明显。如果你在导出时发现内存不足可以加一句--device cpu强制CPU导出。导出完成后检查一下目录ls -l models/Qwen2.5-1.5B-ONNX正常会有model.onnx和model.onnx_data两个文件前者是权重分离后的图结构后者是实际参数。在下游转换时工具链会自动寻找同目录下的外部权重文件所以这两个文件必须放在同一个目录下不要手动拆开。3.2 INT8量化与MTK格式转换拿到ONNX只是第一步接下来真正体现MTK工具链价值的就是量化和格式转换。MTK工具链提供统一入口直接一步到位不用自己写PTQ脚本。mtk_gai convert \ --input ./models/Qwen2.5-1.5B-ONNX/model.onnx \ --output ./output/qwen2.5-1.5B-int8.mtk \ --precision int8 \ --tokenizer ./models/Qwen2.5-1.5B-Instruct/tokenizer.json \ --calibration_data ./calib_data.txt--calibration_data是校准集工具链会从中采样一批数据计算激活值的分布用于选择合适的量化scale和zero point。校准集不需要很大我这次用了300条中文问答数据耗时差不多8分钟效果足够了。量化精度从fp32到int8可选具体选择看硬件支持情况。天玑平台新一代芯片对int8支持很好所以生产环境建议直接int8。int4量化虽然能进一步压体积但端侧runtime对int4算子的优化程度不如int8推理速度未必更快。转换过程会输出一份量化和精度报告类似这样[INFO] Per-tensor quantization applied to 128 linear layers [INFO] Avg cosine similarity: 0.9823 [INFO] Output file: ./output/qwen2.5-1.5B-int8.mtk如果余弦相似度低于0.96就要检查校准集是否太偏或者是否混入了空白数据。3.3 量化参数与精度对比很多人看到量化就担忧生成质量崩坏实际上Qwen2.5-1.5B在int8量化后的文本质量下降很小。我拿同样一段Prompt分别跑了FP16原模型和INT8量化模型做了个简单对比项目FP16原模型INT8量化模型权重文件大小约3.0GB约0.8GB50字回答耗时纯CPU约28秒约15秒ROUGE-L摘要任务0.610.59直观感受回答更丰富偶尔用词简略从表格能看出来INT8量化在体积和速度上优势巨大代价主要是生成内容略微“收敛”。如果场景是聊天机器人这个损失完全可接受如果是做专业领域的精准改写任务建议保留FP16作为备选模型。转换结束后我会用工具链自带的模拟器在PC上先跑一遍余弦相似度和生成样例确认没问题再动Android工程。4. Android端侧部署实战4.1 工程结构与依赖配置MTK GAI Toolkit配套的Android runtime以.so动态库和头文件方式提供。拿到官方NDK包后解压到项目里的libs和include目录然后配置CMakeLists.txt。cmake_minimum_required(VERSION 3.22.1) project(qwen_android) add_library(mtk_gai_runtime SHARED IMPORTED) set_target_properties(mtk_gai_runtime PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libmtk_gai_runtime.so ) add_library(qwen_jni SHARED qwen_jni.cpp) target_link_libraries(qwen_jni mtk_gai_runtime android log ) include_directories(${CMAKE_SOURCE_DIR}/include)这里有个很容易踩的坑ANDROID_ABI不要图省事只编arm64-v8a因为部分天玑工程机还会用到armeabi-v7a的测试包两个ABI的so文件名可能相同但内部指令集完全不同。我在联调阶段就吃过亏一直只编arm64换到同事的32位进程测试时崩溃到只能看backtrace。把转换好的.mtk模型文件拷贝到app/src/main/assets/models/目录然后在JNI层实现加载和推理。4.2 模型加载与推理代码实现核心逻辑其实不复杂但配合JNI、模型加载、tokenizer转换三部分代码量就上来了。先看加载模型的最小示例#include mtk_gai/runtime.h MtkGaiModel* model nullptr; MtkGaiStatus status MtkGaiCreateModelFromFile( /data/user/0/com.example.qwen/files/models/qwen2.5-1.5B-int8.mtk, model); if (status ! MTK_GAI_OK) { __android_log_print(ANDROID_LOG_ERROR, QwenJNI, load model failed: %d, status); return; }模型加载完成后就是推理。Qwen是自回归模型最关键的是维护好past_key_values缓存。MTK工具链提供了一个MtkGaiTokenizer类输入文本后返回input_ids和attention_mask推理循环里每步把新生成的token追加进输入序列。MtkGaiGeneratedText output; MtkGaiGenerateParams params {}; params.max_new_tokens 64; params.temperature 0.7f; params.top_p 0.9f; params.do_sample true; MtkGaiGenerate(model, tokenizer, 介绍一下端侧AI部署, params, output);注意params里的do_sample在低成本设备上建议关闭。原因是采样模式每次生成会引入随机数端侧runtime为了保持输出稳定会额外做同步间接拖慢速度。如果业务不要求创意发散用greedy解码就好实测能省8%-12%的时间。4.3 性能监控与调优部署不是跑起来就算完要能量化端侧表现。我在MainActivity里写了个简单的计时函数fun measureInference(seconds: Int) { val start System.nanoTime() repeat(seconds) { nativeGenerate(今天天气怎么样) } val elapsed (System.nanoTime() - start) / 1_000_000_000.0 Log.d(QwenBenchmark, average latency: ${elapsed / seconds}s per turn) }测试时我会固定设备到飞行模式、清除白名单以外后台应用保证测试结果不是被微信提醒拖累的。实测数据天玑9300工程机INT8量化输入15字输出64 token首token延迟约0.3秒后续平均每个token约25毫秒。天玑8300工程机同样输入和输出每个token约35毫秒差异完全来自不同芯片的NPU调度策略。如果换成8线程CPU纯算速度只有NPU的大约一半功耗却高出百分之六十。所以调优优先级很清楚优先确认NPU参与推理再调整线程数和量化精度。如果日志里能看到MtkGaiRun耗时集中在nn_execute说明NPU正在工作相反如果大量时间花在memcpy就要检查输入输出是不是有额外的数据拷贝。5. 常见问题与避坑实录5.1 下载慢与文件不完整HuggingFace模型仓库包含多个LFS大文件如果只想要主模型权重别用git clone改用huggingface-cli download并尽量指定include过滤huggingface-cli download Qwen/Qwen2.5-1.5B-Instruct \ --include *.json *.safetensors *.txt \ --local-dir ./models/Qwen2.5-1.5B-Instruct下载中断的情况我遇到不下三次表现是模型加载时报Error: No such file or directory。解决方法是先删除不完整的目录再重新拉取不要指望断点续传能救回半截文件。另外校验时重点盯model.safetensors.index.json里的总大小和实际文件大小对不上就是缺块。5.2 算子不支持与转换报错MTK工具链对ONNX算子集支持很全但Qwen2.5系列里的某些新算子比如RoPE相关的高维reshape在旧opset下可能报Unsupported operator。我一开始用opset 17转换失败降到opset 14就正常了。如果你的模型版本更新先试着逐步降低opset不要一上来就改模型结构。还有一个隐蔽问题text-generation-with-past导出的模型有/past_key_values动态axis如果工具链版本较旧可能要求固定序列长度否则转换过程中报维度推导失败。此时可以在导出命令里加上--batch_size 1 --seq_len 128把输入动态维度固定到128虽然牺牲了一点长度灵活性但性能和兼容性都能提升。5.3 端侧内存爆炸与线程数优化工程机内存普遍够用但推理时仍可能因为运行时把权重复制到了连续内存导致峰值飙升。解决办法是把模型加载方式设为MtkGaiCreateModelFromFile而不是FromBuffer后者会多占用一份文件大小的内存。线程数也不是越多越好。MTK NPU有自己的调度逻辑线程数设置超过6反而会导致CPU和NPU互相抢资源。经过几次试验我最终在双大核设备上把线程数设为4性能最稳定。如果是纯CPU推理还需要关掉自动线程扩展MtkGaiSetConfig(model, MTK_GAI_CONFIG_CPU_THREADS, 4); MtkGaiSetConfig(model, MTK_GAI_CONFIG_CPU_AFFINITY, MTK_GAI_AFFINITY_BIG_CORES);这里有个容易疏忽的点关闭大小核迁移后设备发热降频时会触发runtime的重调度如果不在同一线程里做推理容易出现偶发的长时间卡顿。所以我的JNI代码里用了pthread_setaffinity_np把推理线程绑定到一个固定的核心集群。5.4 部署到非天玑平台时的兼容表现我在一台骁龙平台的开发板上也试过跑同样的.mtk模型runtime直接返回MTK_GAI_ERR_UNSUPPORTED_DEVICE这是预期行为。MTK GAI Toolkit是绑定天玑NPU的转换后的格式不能在非MTK平台运行这点需要提前和团队对齐。如果当前项目需要多平台覆盖可以额外导出一份TFLite或者ONNX Runtime的模型作为兜底。还有个小细节模型转换时用到的校准集最好从目标部署场景的真实数据里抽而不是网上随便找一份。有次我偷懒用了英文语料校准部署后模型对中文长文本的生成风格变得很飘后来换回中文问答数据问题就消失了。写在最后这次实战从拉模型到端侧跑通整个过程比我想象中要顺但也确实在算子上卡过几次。MTK GAI Toolkit目前的完成度已经可以支撑产品级落地关键是要把模型转换、量化校准、端侧runtime这几个环节串联起来理解不要只是在PC上把benchmark跑出来就收工端侧真实设备上的内存压力和功耗才决定最终能不能上架。最后再分享一个小技巧转换后的.mtk文件最好不要用中文路径旧版本工具链对非ASCII路径处理有bug我在CI流水线上因为服务器home带着中文用户名折腾了一下午换成纯英文路径后一次通过。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →