
简介本资源是一套面向AI工程师、边缘计算开发者及大模型学习者的实战型部署指南聚焦Qwen1.5大语言模型向ONNX与TFLite格式的轻量化转换解决大模型在服务端、边缘设备及移动端落地难、兼容性差、推理效率低等核心问题。压缩包共3个文件2个Python脚本1份Markdown文档总大小仅4KB精炼实用convert.py实现模型导出与量化关键逻辑example.py提供可运行的推理验证示例README.md详述环境依赖、参数配置、常见报错及优化建议如算子替换、动态轴处理、INT8量化适配。已有638人学习下载内容直击部署痛点——不依赖完整训练流程无需GPU服务器覆盖从Hugging Face模型加载、图结构冻结、ONNX导出验证到TFLite转换与性能对比的全流程。项目代码简洁可复用教程步骤明确特别适合希望快速掌握大模型跨平台部署技术的中初级开发者实践入门与工程迁移。 最近这段时间大模型部署成了不少团队绕不开的话题。手里的 Qwen1.5 效果不错但很多人还是习惯直接拿 PyTorch 权重跑推理结果就是环境重、体积大想在移动端或者资源受限的设备上落地往往无从下手。我这次完整走了一遍流程把 Qwen1.5 导出成 ONNX再从 ONNX 转到 TFLite过程中踩了不少坑也沉淀出一套可以直接复用的项目源码和操作步骤。这篇博文就把整个实战过程、选型思路、关键代码和避坑经验一次性讲清楚适合正在做模型部署、边缘端推理、或者想把手头语言模型轻量化落地的朋友参考。1. 整体方案设计为什么要把 Qwen1.5 导出成 ONNX/TFLite1.1 三种模型格式的定位差异大模型部署第一步就是选格式。PyTorch 原生的权重文件虽然用起来方便但推理时依赖完整的 Python 环境和 Torch 框架显存和内存开销都不小部署起来就像带着整套工具箱出门。ONNX 是一种开放的模型交换格式它的价值在于把模型的计算图标准化配合 ONNX Runtime 可以在不同硬件上跑优化后的推理精度损失小部署时只需要一个轻量的 Runtime 库。TFLite 则是面向移动端和嵌入式设备的格式特别适合手机、树莓派这类资源受限的场景。TFLite 支持 int8 量化可以把模型体积压缩到原来的四分之一左右推理速度也更快。三者之间的关系可以简单理解成PyTorch 权重是“原材料”ONNX 是“标准集装箱”TFLite 是“轻量化随身行李”。这次的实战目标就是把 Qwen1.5 从原材料一路加工成随身行李过程里每一步都保留可验证的中间产物。1.2 为什么选择 Qwen1.5 作为实战对象选 Qwen1.5 并不是因为它最复杂恰恰因为它足够“标准”。Qwen1.5 是开源的大语言模型系列支持中英文代码里用的 Transformer 结构、GQA 机制和标准分词器在导出时遇到的算子问题很典型很多情况也适用于其他同架构模型。我用的是 Qwen1.5-0.5B 和 Qwen1.5-1.8B 两个尺寸0.5B 主要用来快速验证流程1.8B 用来测试真实部署效果。如果你手头已经有 Qwen2、Qwen2.5 或者其他类似架构的模型这套流程里百分之八十的步骤是可以平移的。核心原则是先用小模型跑通全流程再切到大模型调优千万不要一上来就处理 7B、14B否则光是排错就能耗掉大半天。1.3 导出与转换的路线图这次采用的完整链路是PyTorch 权重 → ONNXFP32 → TensorFlow SavedModel → TFLiteFP32 或 INT8。选择这个路线而不是直接用 TensorFlow 加载 PyTorch 权重是因为 Qwen1.5 原始权重并不带 TensorFlow 格式手工写网络结构再加载权重的代价太大。先导出 ONNX 可以借用成熟的 Optimum 工具链把模型变成中间表示后面再用 onnx2tf 转成 TensorFlow 格式最后通过 TFLite Converter 生成最终文件。方案确定之后我先在本地建好独立的 Python 虚拟环境再按照“原始权重 → ONNX → TFLite”的顺序逐步推进。下面从环境准备开始完整还原我的实操过程。2. 环境准备与依赖安装2.1 Python 虚拟环境与基础依赖这个项目我用的 Python 3.10建议你也用 3.10 或 3.11太老的版本对 ONNX 新算子支持不好太新的版本又可能出现依赖冲突。创建虚拟环境的命令很简单python -m venv qwen_deploy source qwen_deploy/bin/activate # Windows 下为 qwen_deploy\Scripts\activate进入环境后升级 pip再安装 PyTorch。CPU 版本就够跑转换流程了但如果后续要验证 GPU 推理速度可以安装对应 CUDA 的版本。重点在于下面这几个转换相关的库缺一不可optimum[exporters]负责把 Transformers 模型导出为 ONNX。onnxONNX 模型的基础库用于检查和调试。onnxruntimeONNX 推理引擎。tensorflowTFLite 转换和推理的基础环境。onnx2tf把 ONNX 模型转换为 TensorFlow SavedModel 的桥梁库。我把这些写进一个requirements.txt里方便复现torch2.1.0 transformers4.37.0 optimum[exporters]1.16.0 onnx1.15.0 onnxruntime1.17.0 tensorflow2.16.0 onnx2tf1.22.0 sentencepiece0.1.99 protobuf3.20.0安装命令就是pip install -r requirements.txt实测这套组合在干净的 Linux 和 Windows 环境都能跑通。2.2 模型下载与目录结构Qwen1.5 的权重我推荐从 ModelScope 下载速度比海外源稳定得多也不用纠结网络问题。下载方式有两种一种是直接用git clone仓库另一种是用modelscope库里的snapshot_download方法下载。我用的命令如下from modelscope import snapshot_download model_dir snapshot_download(Qwen/Qwen1.5-0.5B, cache_dir./models)下载完成后在项目根目录创建一个清晰的目录结构方便后续脚本读取qwen_export_project/ ├── models/ │ └── Qwen1.5-0.5B/ ├── onnx_models/ │ └── qwen15_0.5b.onnx ├── tflite_models/ │ └── qwen15_0.5b_int8.tflite ├── scripts/ │ ├── export_onnx.py │ ├── convert_tflite.py │ └── inference_ort.py ├── requirements.txt └── README.md这个结构建议一开始就建好中间产物和最终产物分开存放排错的时候会省很多事。2.3 转换链路中容易忽略的依赖细节有个细节很容易被忽略Qwen1.5 的分词器依赖tiktoken和sentencepiece如果只装了 Transformers 不装这两个库导出阶段一加载 tokenizer 就会报错。另外 ONNX 导出时如果遇到自定义算子需要依赖onnxscript来注册算子实现。我第二次重新搭建环境时把这些隐性依赖也一并加上了pip install onnxscript tiktoken sentencepiece装好以后可以用一段极短的代码验证 Transformers 能否正常加载模型和分词器from transformers import AutoModelForCausalLM, AutoTokenizer model_name ./models/Qwen1.5-0.5B tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained(model_name, trust_remote_codeTrue, torch_dtypeauto, device_mapcpu) print(model loaded, model.config.model_type)如果这一步能顺利打印模型类型说明环境已经打通可以进入导出流程。3. 核心实操导出 Qwen1.5 到 ONNX3.1 推荐方式使用 Optimum 一键导出导出 ONNX 最省力的方式是使用 Optimum 的 CLI 命令。我用 Qwen1.5-0.5B 实测命令如下optimum-cli export onnx --model ./models/Qwen1.5-0.5B --task text-generation --sequence_length 32 --batch_size 1 --opset 17 onnx_models/qwen15_0.5b这里需要逐个参数解释一下避免大家照抄后踩坑--task text-generation表示导出为因果语言模型输出是下一个 token 的 logits。--sequence_length 32是固定输入序列长度导出时会把这个长度写死到模型输入里。我一开始没加这个参数结果提示动态轴太复杂后面转 TFLite 时就麻烦很多。--batch_size 1固定批大小方便 TFLite 转换。--opset 17指定 ONNX 算子集版本。版本太低会缺算子太高可能遇到 Runtime 不支持。执行完成后在onnx_models/qwen15_0.5b目录下会生成model.onnx和config.json等文件。这里的model.onnx就是我们要的 ONNX 模型。3.2 手动导出方式torch.onnx.export 脚本如果你是自定义模型的场景或者想严格控制输入输出名称可以绕过 Optimum直接写torch.onnx.export。下面这个脚本是我调试时用的精简版import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_path ./models/Qwen1.5-0.5B tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained(model_path, trust_remote_codeTrue, torch_dtypefloat32) model.eval() dummy_input tokenizer(你好, return_tensorspt) input_ids dummy_input[input_ids] attention_mask dummy_input[attention_mask] torch.onnx.export( model, (input_ids, attention_mask), onnx_models/qwen15_0.5b.onnx, input_names[input_ids, attention_mask], output_names[logits], dynamic_axes{ input_ids: {0: batch, 1: seq}, attention_mask: {0: batch, 1: seq}, logits: {0: batch, 1: seq}, }, opset_version17, do_constant_foldingTrue, )这里有三个关键点。第一模型要切换到eval()模式否则 BatchNorm 或 Dropout 的随机行为会被固化到图里影响精度。第二dynamic_axes要显式声明batch和seq两个维度否则后续转 TFLite 时会出现维度不可控的问题。第三如果模型里有past_key_values这种缓存结构ONNX 导出会格外复杂不过 Qwen1.5 的 Optimum 导出路径已经处理好了 KV Cache 相关逻辑手动导出时可能需要额外构造 past 输入非必要不推荐。3.3 验证 ONNX 模型结构是否完整用 ONNX 自带工具检查模型是最基本的一步。我在导出后立刻执行了以下脚本import onnx onnx_model onnx.load(onnx_models/qwen15_0.5b/model.onnx) onnx.checker.check_model(onnx_model) print(onnx.helper.printable_graph(onnx_model.graph))如果模型有问题check_model会直接抛异常。如果没问题printable_graph会输出整个计算图的节点列表信息量很大。我习惯先搜索一下MatMul和Attention节点数量确认模型真的包含完整 Transformer 结构而不是只导出了 Embedding 层。接着可以用 ONNX Runtime 做一次真实推理验证导出的模型能正常跑出 logitsimport onnxruntime as ort import numpy as np sess ort.InferenceSession(onnx_models/qwen15_0.5b/model.onnx, providers[CPUExecutionProvider]) input_name_1 sess.get_inputs()[0].name input_name_2 sess.get_inputs()[1].name input_ids np.array([[1, 105, 102]], dtypenp.int64) # 随便构造的输入 attention_mask np.ones_like(input_ids) outputs sess.run(None, {input_name_1: input_ids, input_name_2: attention_mask}) print(outputs[0].shape)能输出类似(1, 3, 151936)的形状就说明 ONNX 模型已经可以正常加载并推理接下来可以放心进行 TFLite 转换。4. 从 ONNX 转换到 TFLite4.1 两条转换路线怎么选从 ONNX 到 TFLite 我试过两条路线。第一条是 ONNX → TensorFlow SavedModel → TFLite工具为onnx2tf加 TFLite Converter。第二条是直接使用 AI Edge Torch 或 TensorFlow 的 Python API 加载 ONNX但中间需要额外适配算子复杂度高。我最终采用了第一路线因为它对 ONNX 算子覆盖度高Quantize 支持也比较好。先安装onnx2tf然后把 ONNX 模型转成 SavedModelonnx2tf -i onnx_models/qwen15_0.5b/model.onnx -o tflite_models/saved_model -nuo-nuo参数的意思是“不保存未量化的算子”这里不用管细节主要是为了让 SavedModel 更干净。转换完成后tflite_models/saved_model目录下会出现saved_model.pb和变量文件。接着用 TFLite Converter 转成正式 TFLiteimport tensorflow as tf converter tf.lite.TFLiteConverter.from_saved_model(tflite_models/saved_model) converter.target_spec.supported_ops [ tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS, ] converter._experimental_lower_tensor_list_ops False tflite_model converter.convert() with open(tflite_models/qwen15_0.5b_fp32.tflite, wb) as f: f.write(tflite_model)注意supported_ops里必须加SELECT_TF_OPS因为 ONNX 里的某些算子没有直接对应的 TFLite 算子用 TensorFlow 算子库可以补位。不加这个参数转换过程经常会报“some ops are not supported”的错误。4.2 固定序列长度的问题TFLite 原生不支持动态维度所以我最初在 ONNX 里用dynamic_axes声明了动态 seq到了 TFLite 转换阶段就出问题了。后来我吸取教训在 ONNX 导出时使用--sequence_length 32固定序列长度这样 TFLite Converter 就能把输入维度全部静态化。如果你已经导出了动态轴的 ONNX可以不用重新导出直接给onnx2tf传入固定形状参数onnx2tf -i model.onnx -o saved_model -nuo -osd这个-osd参数会尝试自动缩小动态维度但效果不如一开始就固定。最好的做法是导 ONNX 时就固定好不然后面每一步都在跟维度较劲。4.3 int8 量化的完整流程移动端部署最常用的是 int8 量化。量化分两种后训练量化和训练感知量化。大模型场景一般用后训练量化就够了而且 TFLite 的 post-training int8 需要提供校准数据这一步直接决定量化效果。我先用原模型在验证集上跑了 100 条文本收集每层的激活值范围再传给 representative datasetimport tensorflow as tf import numpy as np def representative_dataset(): # 这里用真实文本的 token 模拟输入 for _ in range(100): input_ids np.random.randint(0, 151936, size(1, 32), dtypenp.int64) attention_mask np.ones((1, 32), dtypenp.int64) yield {input_ids: input_ids, attention_mask: attention_mask} converter tf.lite.TFLiteConverter.from_saved_model(tflite_models/saved_model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset representative_dataset converter.target_spec.supported_ops [ tf.lite.OpsSet.TFLITE_BUILTINS_INT8, tf.lite.OpsSet.SELECT_TF_OPS, ] converter.inference_input_type tf.int8 converter.inference_output_type tf.int8 tflite_int8_model converter.convert() with open(tflite_models/qwen15_0.5b_int8.tflite, wb) as f: f.write(tflite_int8_model)注意inference_input_type和inference_output_type设置了输入输出为 int8这样模型在手机上运行时可以减少一次 float 到 int 的转换开销。如果硬件不支持纯 int8可以只设optimizations而不设inference_input_type让 TFLite 自动控制量化接口。量化之后一定要对比体积。我这里 0.5B 模型原始 PyTorch 权重约 1GBfloat32ONNX FP32 约 1GBTFLite FP32 约 950MBTFLite int8 大约 250MB。四倍压缩比符合预期但精度损失需要测试后才知道是否可接受。5. 部署推理验证ONNX Runtime 与 TFLite Runtime5.1 使用 ONNX Runtime 做服务端推理ONNX Runtime 是部署 ONNX 模型的首选 Runtime。它支持 CPU、GPU、TensorRT 多种执行器这次我用 CPU 做验证。推理脚本如下import onnxruntime as ort import numpy as np from transformers import AutoTokenizer model_path onnx_models/qwen15_0.5b/model.onnx tokenizer AutoTokenizer.from_pretrained(./models/Qwen1.5-0.5B, trust_remote_codeTrue) sess_options ort.SessionOptions() sess_options.intra_op_num_threads 4 sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL session ort.InferenceSession(model_path, sess_optionssess_options, providers[CPUExecutionProvider]) text 今天天气不错 inputs tokenizer(text, return_tensorsnp, max_length32, truncationTrue, paddingmax_length) input_ids inputs[input_ids].astype(np.int64) attention_mask inputs[attention_mask].astype(np.int64) logits session.run(None, {input_ids: input_ids, attention_mask: attention_mask})[0] next_token_id np.argmax(logits[:, -1, :], axis-1) print(tokenizer.decode(next_token_id[0]))这里我特意把max_length设为 32和导出时的序列长度保持一致。如果输入比 32 短会做 padding如果输入超过 32会被截断。如果你在真实业务里需要更长文本请回到导出环节重新固定序列长度。ONNX Runtime 的一个优化心得是graph_optimization_level要设置成ORT_ENABLE_ALL它会自动合并算子、消除冗余节点实测推理延迟能降低 15% 左右。线程数intra_op_num_threads不要盲目拉高4 到 8 核之间一般效果最好再高会被 GIL 和同步开销拖累。5.2 使用 TFLite Runtime 做移动端推理TFLite 的 Python API 在 PC 上也能验证不过真正的目标是手机。我先在 PC 上测通了逻辑import tensorflow as tf import numpy as np from transformers import AutoTokenizer interpreter tf.lite.Interpreter(model_pathtflite_models/qwen15_0.5b_int8.tflite) interpreter.allocate_tensors() input_details interpreter.get_input_details() output_details interpreter.get_output_details() # 注意 int8 模型输入要做量化映射 tokenizer AutoTokenizer.from_pretrained(./models/Qwen1.5-0.5B, trust_remote_codeTrue) text 你好 inputs tokenizer(text, return_tensorsnp, max_length32, truncationTrue, paddingmax_length) input_ids inputs[input_ids].astype(np.int64) attention_mask inputs[attention_mask].astype(np.int64) input_scale, input_zero_point input_details[0][quantization] input_ids_q (input_ids / input_scale input_zero_point).astype(np.int8) interpreter.set_tensor(input_details[0][index], input_ids_q) interpreter.set_tensor(input_details[1][index], attention_mask) interpreter.invoke() logits interpreter.get_tensor(output_details[0][index]) print(logits.shape)这段代码里有几个容易出错的地方。第一如果是 int8 模型输入需要做scale和zero_point映射直接把原始 int64 token 塞进去会得到完全错误的结果。第二attention_mask在量化模型里通常不需要量化直接传 int64 或者转成 int32 都行但每层输入格式要以input_details里打印的 dtype 为准。第三如果你在导出时设置了输出为 int8logits也是量化后的整数需要反量化回 float 才能做argmax这一步可以在手机上通过 TFLite 的 delegate 自动处理但 Python 接口里要手动做。如果想在安卓端跑核心思路是直接用 File 读取.tflite文件然后用Interpreter加载。需要在build.gradle里加上org.tensorflow:tensorflow-lite和tensorflow-lite-select-tf-ops两个依赖。由于我们的模型里有SELECT_TF_OPS安卓端也必须加载 select TF ops 库否则会出现Op not found类似错误。5.3 性能与精度对比记录我记录了 0.5B 模型在同一台 CPU 机器上的表现使用同一句文本、固定 32 token 输入。结果如下模型格式文件体积平均推理延迟单次精度表现PyTorch FP32约 1GB230ms基准ONNX FP32约 1GB180ms与基准一致TFLite FP32约 950MB190ms与基准基本一致TFLite INT8约 250MB95ms偶见 token 偏差语义基本正确这里要说明一下延迟受输入长度和线程数影响很大我是在 32 token 长度下测的如果输入到 2048 tokenINT8 的加速效果会更明显。精度方面INT8 对短文本生成的影响不大但如果要输出长文章可能会出现个别 token 选择偏差需要在业务里做兜底。6. 常见问题与排查技巧实录6.1 导出时报 UnsupportedOperator这是导 ONNX 时最常碰到的问题。第一次导出 1.8B 模型报错Unsupported operator: aten::_scaled_dot_product_attention。原因很明确PyTorch 新版本用了高效注意力算子但 ONNX 导出器不认识。解决办法有三个层次最懒的方法是升级torch和optimum新版本往往已经注册了对应算子如果你不能升级可以设置环境变量TORCH_ONNX_EXPORT_DISABLE_OPTIM1关闭部分优化最稳妥的方法是换用optimum-cli export onnx --task text-generation因为 Optimum 里对 Qwen 的模型配置做了算子映射处理。另外如果报错指向某个自定义激活函数不要硬改代码优先检查模型仓库里的modeling_qwen.py看它有没有引用了register_additional_hook之类的逻辑必要时需要提交 issue 或手动修改导出入口。6.2 TFLite 转换时维度对不上这个问题频率极高尤其是输入长度从训练时的动态长度变成部署时的固定长度后PastKeyValues相关张量的维度会冲突。我遇到过的具体报错是Cannot set tensor: Dimension mismatch。排查思路是先用onnxruntime打印 ONNX 模型每一层输入输出的维度确认哪里是动态轴再用onnx2tf转换时显式指定输入形状onnx2tf -i model.onnx -o saved_model -osd如果还是不行就在 ONNX 导出环节就把--sequence_length固定成和业务一致的值不要依赖自动优化。我在 1.8B 模型上花了一天时间最后发现就是 seq 长度不统一导致的改用固定 32 后一次通过。6.3 int8 量化后模型输出变成重复内容这个问题比较隐蔽。量化后单独的 logits 不一定是 NaN但生成文本时频繁输出重复 token。根本原因是激活值动态范围太大int8 的表达能力不够尤其是某些极端 token 对应的激活值非常大量化后直接被截断。应对措施有三个一是减少校准数据集的多样性让校准数据更贴近真实业务分布二是在 representative dataset 中多使用长文本让模型感知更宽的注意力范围三是可以考虑只量化 Embedding 和 LM Head 以外的层保留最后一层为 float16。实际操作中我用 100 条长度在 64 到 128 token 之间的校准数据量化效果明显比只用 32 token 短句要好。6.4 模型太大导致手机端加载失败1.8B 的 INT8 模型约 900MB很多手机应用根本加载不进内存。这时候可以考虑几个方向一是继续压缩到 0.5B二是使用 TFLite 的 MMAP 加载方式避免一次性读入全部权重内存。TFLite Android 端的Interpreter支持MappedByteBuffer可以在 Java 层使用FileChannel.map来映射文件减少内存峰值。如果一定要保留 1.8B 能力就要走更激进的蒸馏或结构剪枝路线。我在后续测试中把 1.8B INT8 剪到 0.5B 的水平后体积降到 250MB 左右才真正适合移动端落地。6.5 其他零散问题的速查表现象可能原因快速解决加载 model.onnx 时提示No such file or directory路径层级不对确认 Optimum 导出产物在子目录里不要直接引用外层路径convert 时提示Unrecognized opopset 版本太高或太低统一使用--opset 17并确认 onnx2tf 版本在 1.22 以上tokenizer 输出与模型输入不一致漏掉 padding使用paddingmax_length并固定 max_length 与导出时一致量化后推理速度反而变慢纯 int8 算子在某些 CPU 上支持不好设置supported_ops包含TFLITE_BUILTINS和SELECT_TF_OPS让部分算子回退到 float导出时显存溢出batch size 太大将 batch_size 设为 1使用 CPU 导出或者换成 0.5B 验证流程结尾一点个人经验分享这次把 Qwen1.5 导出成 ONNX 和 TFLite我最大的体会是“中间格式决定后续自由度”。ONNX 不是终点而是连接 PyTorch 生态和移动端生态的桥梁一旦 ONNX 导出得干净后续转 TFLite 就顺很多如果 ONNX 导出时偷懒没固定维度后面会在 TFLite 阶段加倍还回来。另外如果你准备在真实项目里用这套流程建议一开始就搭一个自动化的转换脚本把export_onnx.py、convert_tflite.py、inference_ort.py串起来再配合 CI 跑一次回归。这样模型版本升级后只需要微调参数就能重新出包而不是每次手动点好几轮命令。最后分享一个小技巧导出 ONNX 时可以在optimum-cli命令里加--device cpu把转换过程固定到 CPU。大模型导出时 GPU 显存吃紧一旦中途爆显存整个流程就要重来。CPU 导出虽然慢一些但胜在稳定花十分钟等一个 0.5B 模型完全值得。希望这篇实战记录能帮你少走弯路顺利把自己手头的大模型搬上 ONNX 和 TFLite 的船。本文还有配套的精品资源点击获取