
坦白讲Qwen2.5-1.5B这个尺寸放在端侧属于“刚刚好”的甜点级选择。大模型在云上跑得欢但一落到手机、平板、开发板上内存和算力就开始捉襟见肘这时候MTK GAI Toolkit的价值就出来了。这篇文章会把整个流程串起来从Hugging Face上拉模型到MTK工具链做转换、量化最后在端侧runtime里跑起来。我会尽量把每个环节的“为什么这么做”讲清楚也会把我踩过的坑、调参时的取舍一并写出来给准备在MediaTek平台上落地小模型的你做个参考。1. 内容整体设计与思路拆解1.1 为什么选Qwen2.5-1.5B作为端侧模型先说结论1.5B这个量级刚好卡在“能力可接受”和“端侧跑得动”的交叉点上。Qwen2.5-1.5B是Qwen2.5系列里比较轻量的成员参数量约15亿但依然保留了分组查询注意力GQA、更强的指令跟随和长上下文支持这些特性。和0.5B版本相比它的生成质量和指令理解明显扎实和7B版本相比它的内存占用和推理延迟又友好太多。在MTK这类中高端SoC上跑1.5B内存占用大约在2GB到3GB之间取决于序列长度和量化位数这个数字对旗舰手机或带有4GB以上RAM的Linux开发板来说是可以接受的。选择Hugging Face作为模型源也很自然。Qwen系列官方权重都托管在Hugging Face上模型结构、tokenizer配置、推理代码都是标准化格式方便做二次加工。MTK GAI Toolkit本身并不依赖具体模型厂商它更像是一个“翻译层”负责把PyTorch或ONNX形态的模型转换成MediaTek硬件平台能高效运行的中间表示。所以用Qwen2.5-1.5B来过一遍全流程比直接用Qwen2.5-7B要省心很多最适合作为熟悉工具链的入门项目。1.2 MTK GAI Toolkit的核心工作流拆解MTK GAI Toolkit从名字看是联发科围绕生成式AI推出的一整套工具包。它的输入可以是Hugging Face上的PyTorch模型权重也可以是ONNX模型经过它内部的转换器、优化器和量化器处理后最终输出一个带版本号、带配置描述符的模型包再交给端侧推理的runtime加载。整个流程可以拆成四条主线模型获取从Hugging Face下载模型配置、词表、权重文件确认本地文件结构的完整性。模型转换将PyTorch的nn.Module导出为静态或动态shape的ONNX图再用MTK工具把ONNX编译成自家runtime认识的数据布局。模型量化通过PTQ或QAT将FP32权重压缩为INT8甚至INT4减少内存和计算量。MTK工具链里这一步通常是在转换过程中一起完成的。端侧集成把生成的模型包放进Android或嵌入式工程的资源目录在应用层通过runtime API加载模型、执行推理。这四条线不是独立存在的而是环环相扣。比如转换时如果选了动态batch量化时就要额外处理校准数据集的输入维度如果端侧需要极低延迟可能就得放弃batch推理走单序列流式生成这会影响转换时的优化选项。所以动手之前先想清楚目标硬件的内存上限、允许的模型大小、期望的首token延迟再来决定每一步的参数。2. 环境准备与模型获取2.1 开发机环境配置与依赖安装我建议直接准备一台带NVIDIA GPU的Linux机器内存32GB以上显存8GB以上。虽然1.5B模型在纯CPU上也能导出但做校准和验证时CPU实在太慢用GPU能省一个数量级的时间。系统层面需要Python 3.10或3.11CUDAToolkit 12.xPyTorch 2.1以上。MTK GAI Toolkit的安装包一般会以Python wheel或SDK压缩包的形式分发具体以你手上的版本为准但核心依赖几乎就是transformers、accelerate、onnx、onnxruntime、numpy这几项。登录设备后先创建虚拟环境python -m venv mtk_gai_env source mtk_gai_env/bin/activate pip install --upgrade pip pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install transformers accelerate huggingface_hub onnx onnxruntime这里有个容易被忽略的细节accelerate和transformers的版本要尽量新。早期版本的transformers在加载Qwen2.5时可能不认识某些配置字段比如qwen2_5模型类型就会直接报KeyError。我的建议是装完后用下面这行确认版本至少是transformers 4.43以上。python -c import transformers; print(transformers.__version__)MTK工具的安装不会自动装这些依赖需要手动补齐。还有一个好习惯是安装huggingface-cli命令行工具虽然最后多半会用Python API但偶尔看看缓存目录、检查下载完整性还是有用的。2.2 使用huggingface_hub从Hugging Face拉取模型模型获取阶段我强烈推荐用snapshot_download而不是transformers的from_pretrained。前者会把整个仓库目录完整拉下来后者只拉取模型加载阶段需要的文件容易漏掉量化配置、README里的说明文件或generation_config.json后续排查时缺少参考信息。一个稳妥的下载脚本如下from huggingface_hub import snapshot_download repo_id Qwen/Qwen2.5-1.5B-Instruct local_dir ./models/Qwen2.5-1.5B-Instruct snapshot_download( repo_idrepo_id, local_dirlocal_dir, local_dir_use_symlinksFalse, resume_downloadTrue, allow_patterns[ *.json, *.txt, *.safetensors, *.py, ], )如果只想下载权重和必需配置allow_patterns可以精确控制。resume_downloadTrue能断点续传特别是模型文件好几个GB时网络闪断至少不用从头再来。local_dir_use_symlinksFalse会把真实文件直接放到目标目录而不是以缓存硬链接的形式存在方便后面工具直接读取。如果你更习惯命令行也可以用huggingface-cli download Qwen/Qwen2.5-1.5B-Instruct --local-dir ./models/Qwen2.5-1.5B-Instruct我实际用下来觉得Python API在自动化脚本里更好用因为可以接着处理模型文件、解析config不需要跳回shell再跑一次。2.3 模型文件结构与校验下载完以后先别急着拿去转换花两分钟检查一下目录结构是否完整。一个标准的Qwen2.5-1.5B-Instruct仓库里重点文件是这些文件/目录作用config.json模型结构配置包括层数、注意力头数、上下文长度、模型类型等generation_config.json生成时的默认参数如max_new_tokens、temperature、top_ptokenizer.json/tokenizer_config.jsontokenizer序列化配置离线加载必备model-00001-of-00002.safetensors等分片权重1.5B模型一般会切成2到4个文件vocab.json/merges.txt如果使用GPT分词器这两个文件也不能缺如果发现只有model-00001-of-00002.safetensors但缺少第二个分片加载时必然报错。一个快速校验方法是打印所有文件名人工核对或者用transformers尝试加载一遍能在转换前发现90%的“文件缺失”问题from transformers import AutoModelForCausalLM model AutoModelForCausalLM.from_pretrained(./models/Qwen2.5-1.5B-Instruct) print(model.config.model_type)这一步跑通之后模型文件就能进入转换流程了。我在实际项目中遇到过一次很隐蔽的问题tokenizer.json存在但内容被截断了半截加载tokenizer时不报错直到推理阶段才出现乱码。所以有条件的话顺手把tokenizer加载一遍并输出几个样本token成本很低收益很高。3. 模型转换与量化3.1 转换链路选择PyTorch → ONNX → MTK格式MTK GAI Toolkit能直接吃PyTorch模型吗看你拿到的工具版本。早期版本主要接受ONNX新版可能也支持直接从Hugging Face权重转换。但我个人建议还是走ONNX中间格式原因有两个中间格式可见可控。ONNX导出过程中如果出现算子不支持可以马上定位是哪一层直接转换时错误信息经常被工具链吞掉一半排查困难。ONNX结构清晰很多开源生态的优化工具都能介入比如onnxsim简化运算图onnxruntime验证推理结果。先用ONNX验证一下导出的模型能正确跑通再交给MTK转换能有效隔离问题。导出ONNX时重点是用torch.onnx.export显式指定输入张量的shape和name。Qwen2.5这类自回归模型导出一个带past_key_values的生成式ONNX比较复杂。最省事的方法是直接用transformers的model.forward作为导出函数传入一个input_ids和一个可选的past_key_values结构。但由于动态维度处理麻烦很多团队会选择先用静态sequence length导出或者只导出text encoder部分。我这里给出一个简化的静态导出示例作为整个链路的起点import torch from transformers import AutoModelForCausalLM, AutoTokenizer model AutoModelForCausalLM.from_pretrained(./models/Qwen2.5-1.5B-Instruct, torch_dtypefp32) model.eval() tokenizer AutoTokenizer.from_pretrained(./models/Qwen2.5-1.5B-Instruct) inputs tokenizer(你好, return_tensorspt) torch.onnx.export( model, (inputs[input_ids],), qwen2.5-1.5b-input_ids.onnx, input_names[input_ids], output_names[logits], dynamic_axes{input_ids: {0: batch, 1: seq_len}}, opset_version14, do_constant_foldingTrue, )注意这样导出的只是单次前向的logits没法做带KV cache的完整生成。如果要在端侧实现真正的生成式推理需要把past_key_values也建模进去工作量会上一个台阶。MTK工具链的文档通常会提供一个“生成式模型导出模板”建议直接以官方模板为底座而不是自己硬写。我早期自己手搓过一版pytorch算子映射到ONNX时repeat_interleave这种算子经常让转换器直接罢工最后还是回到官方模板上改改。3.2 使用MTK GAI Toolkit完成量化模型文件准备好之后进入MTK GAI Toolkit的核心环节转换和量化。假设你手里有mtk_gai_converter命令行工具基础命令可能是这样mtk_gai_converter \ --input qwen2.5-1.5b-input_ids.onnx \ --output output/qwen2.5-1.5b.mtk \ --quantize ptq \ --calibration_data ./calib_data/ \ --precision int8 \ --input_info input_ids:1x32这个命令的含义是输入模型为ONNX输出MTK格式执行POST-TRAINING QUANTIZATIONPTQ量化目标精度为INT8校准数据目录为./calib_data/输入尺寸是batch1序列长度32。PTQ的校准过程本质上就是让模型“看”一批真实数据统计每个激活层的数值范围然后决定量化scale和zero_point。校准数据集非常关键尤其对生成任务来说输入分布是短文本如果校准数据全是长文档模型会把很多激活范围放大量化后生成质量就会下降一个档次。我在做这个项目时用的是从训练集里随机抽出来的1000条中文短句覆盖日常问答、摘要、代码注释等场景长度统一截断到32个token效果比用一个长文档集合好很多。量化过程中还要注意per-channel和per-tensor的区别。卷积层和矩阵乘法对per-channel更友好但对小模型来说per-channel会引入额外计算开销。MTK工具链通常会根据算子类型自动选择量化粒度但如果你想手动控制可以看看转换器的参数里是否有--quantize-mode mixed。我实际测试下来1.5B模型用per-tensor INT8量化时精度下降还能接受再压到INT4就很容易出现重复生成、答非所问的情况。如果非要INT4建议关键层比如注意力输出层保留INT8做混合精度量化。3.3 量化与转换中的参数取舍我把几个影响较大的参数单独拎出来说。首先是输入序列长度。端侧部署时建议把模型支持的max_position_embeddings从头到尾设成你实际会用到的最大值。Qwen2.5-1.5B的config可能支持32K上下文但端侧内存根本扛不住32K的KV cache。我在设备上通常切成2048即便这样随着生成token数增加KV cache还是会持续膨胀。如果你的应用场景是短对话甚至可以把最大长度压到512转换出来的模型包能小很多启动速度也快。其次是batch size。端侧推理大多数情况batch1所以转换时把batch固定成1就行。如果你在转换时留了动态batch工具链为了支持动态shape会做额外padding多了不少冗余计算推理延迟反而会上去。固定shape的好处是算子融合更彻底内存布局也更紧凑。还有一个容易踩坑的点是opset_version。ONNX标准一路升级新版导出到旧版运行时可能遇到算子版本不兼容。MTK GAI Toolkit支持到什么opset要以官方文档为准。我一般用opset 14或者15兼容性和算子覆盖度比较平衡太新的opset比如18以上反而容易在转换器里翻车。4. 端侧部署与推理实现4.1 确定目标设备与运行时集成转换输出的qwen2.5-1.5b.mtk模型包最终要部署到什么设备上对MTK GAI Toolkit来说典型目标有两个一是Android手机/平板二是嵌入式Linux系统比如MTK改装开发板。不同平台的集成方式不太一样但核心runtime接口是相近的。Android侧需要把mtk_gai_runtime的aar库和模型包一起放进工程通过JNI调用Native授权接口。大致流程是把.mtk模型文件放到assets目录或者首次启动时从网络下载到应用私有目录。在Java/Kotlin层初始化runtime context传入模型路径、线程数、内存上限。用tokenizer对输入文本编码成input_ids。调用生成接口循环执行generate_next_token把当前token和KV cache传入拿回logits后再采样出下一个token。把输出token序列解码回文本。嵌入式Linux侧更直接一般是用C/C API编译时连上libmtk_gai_runtime.so然后自己处理模型加载和推理循环。用起来更底层但也能更精细地控制内存和算子调度。4.2 跑通一个最小推理Demo不管哪种平台我建议先跑通一个不依赖外部UI的纯命令行Demo排除环境问题后再进入总装。以一个简化的C示例为例核心逻辑是#include mtk_gai_runtime.h int main() { MtkGaiContextHandle context MtkGaiCreateContext(); MtkGaiLoadModel(context, /data/local/tmp/qwen2.5-1.5b.mtk); std::vectorint input_ids {151644, 8948, ...}; // tokenizer编码结果 MtkGaiInput input MtgGaiCreateInput(input_ids, input_ids.size()); int new_token 0; do { MtkGaiOutput output MtkGaiForward(context, input); new_token MtkGaiSampleToken(output.logits); input MtkGaiAppendToken(input, new_token); } while (new_token ! tokenizer.eos_token_id); MtkGaiReleaseContext(context); return 0; }真实项目中MTK runtime通常会提供更高层的generate接口封装了采样和停止条件不需要你自己写循环。但理解这个循环仍然重要因为很多指标如prefill时间、decode时间都是在这个循环里测出来的。我在第一次跑通时只输入了“给我讲个笑话”模型输出了几十个字的回复。虽然内容质量一般但看到token一个个蹦出来整个链路就算通了。这时候先别急着优化先把数值结果和PC端ONNX推理结果做对比确认转换和量化没有引入灾难性错误。4.3 性能调优与内存控制跑通之后紧接着就是性能调优。几个关键方向显存/内存预热端侧推理库通常会有独立的内存池。启动时先调用MtkGaiReserveMemory分配好固定大小内存避免推理过程中频繁malloc导致卡顿。线程与CPU绑核运行时提供线程数设置一般设在4到6个线程。可以把大核绑给per decode算子小核留给系统调度能明显降低抖动。KV cache复用每次请求做完后把KV cache清空但不释放内存下一轮请求复用减少重新分配开销。采样参数temperature和top_p不要设太高否则模型容易重复。端侧小模型本来就容易发散我在Demo里用temperature0.7, top_p0.9效果比较稳定。内存优化上有个很实用的指标固定输入长度下不同量化位数的模型占用内存对比。我用1.5B模型简单测过精度模型文件大小峰值内存seq_len512FP32约6GB约7GB以上FP16约3GB约4GBINT8约1.6GB约2.4GBINT4约0.9GB约1.5GB如果你的目标设备只有3GB可用内存INT8是底线起步INT4更稳妥但生成质量需要反复验证。我最终在手机上选了INT8因为它的内存天花板相对安全模型精度损失也不明显。5. 常见问题与排查技巧5.1 高频故障与解决方案速查我之前在做这个项目时问题集中出现在几个地方列一张表给你避坑问题现象可能原因排查方法from_pretrained报KeyError: qwen2_5transformers版本太老升级transformers到4.43及以上ONNX导出时shape mismatch输入shape没有匹配到模型内部张量检查input_ids维度确认batch和seq_len与forward入参一致MTK转换器提示Unsupported operator某些PyTorch算子没有ONNX映射升级工具链版本或者把Model代码里的特殊算子替换为普通算子量化后模型输出乱码tokenizer.json损坏或校准数据分布偏差重新下载tokenizer检查校准集是否有足够多样的样本端侧加载模型崩溃模型包路径不对或内存不足用file命令确认模型包完整性用free -m查内存余量生成速度极慢线程数设置过低或KV cache未复用提高线程数到4以上确认推理循环里复用了past_key_values最高频的还是第一类版本问题。AI工具链迭代速度很快Hugging Face上的模型越来越新而MTK工具包和transformers如果没跟上节奏很容易出现诡异的兼容性报错。我的处理习惯是先在一个干净的虚拟环境里把官方示例跑通再去碰自己的模型能省一半的排查时间。5.2 几个我反复琢磨出来的独门技巧下面这些经验不是文档里会写的但很管用。第一个技巧量化校准集不要只选“内容正确”的数据要选“结构多样”的数据。我一开始只用100条影视评论做校准结果模型能做情感分析但写代码提示词时输出一塌糊涂。后来混入了代码、诗歌、新闻标题量化效果立竿见影。数量不贪多500-1000条足够但覆盖面要广。第二个技巧转换前先用ONNX Runtime跑一遍原始ONNX模型确认数值正常。因为有时PyTorch模型的导出结果本身就有问题不是MTK工具链的锅。用一段固定的prompt输入把ONNX Runtime的输出和PyTorch的输出对比误差在1e-4级别就是正常的若出现个位数级别的偏差那就要回头查导出逻辑。第三个技巧在端侧集成阶段给Runtime加一个--profiler开关或者用系统的性能分析工具记录单算子耗时。很多慢其实不是模型计算造成的而是内存分配或者线程调度。我遇到过一次“生成时每两个token卡一下”的问题查到最后是日志库在每次decode时写磁盘把日志关掉立刻流畅许多。第四个技巧也是我最后想说的。1.5B模型是练手的好选择但真到了产品级一定要复盘用户输入的长度分布。如果大多数问句都很短完全可以训练时把最大序列长度压到256进一步缩小模型包。如果偶尔出现长文本就做滑动窗口截断而不是让模型撑到最大长度。端侧资源就这么点把设计目标定准了整个转换和量化参数就能少走很多弯路。这一套流程走下来我对MTK GAI Toolkit的脾气摸得比较透了。说实话模型转换工具链的很多报错信息都不够友好做不到“一眼定位”但只要按照“先单独验证每个节点再整体串联”的思路走Qwen2.5-1.5B这种量级的模型端侧部署是完全可以自己搞定的。如果你接下来打算上7B甚至更大模型建议在1.5B上面把校准集、量化精度、序列长度这些参数调成一套“可复用基线”再动手后面会省掉大量试错时间。