最近在折腾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精度下权重大约3GB,INT8量化后能压到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 torch==2.1.2 transformers==4.43.2 optimum==1.20.0 onnx==1.16.0 \ huggingface_hub==0.23.0 protobuf sentencepieceMTK GAI Toolkit的安装包目前优先支持Ubuntu 20.04/22.04和Windows 11,Mac也可以装但部分算子映射验证功能不完整。安装命令大概是:
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_ENDPOINT=https://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_code=True) model = AutoModelForCausalLM.from_pretrained(model_dir, trust_remote_code=True, torch_dtype="auto") 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.8GB |
| 50字回答耗时(纯CPU) | 约28秒 | 约15秒 |
| ROUGE-L(摘要任务) | 0.61 | 0.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带着中文用户名折腾了一下午,换成纯英文路径后一次通过。