1. 为什么需要将LLM模型导出为ONNX格式最近在部署大语言模型时我发现很多开发者都面临一个共同难题如何让LLaMA、ChatGlm2这些大家伙能在不同平台上顺畅运行传统PyTorch模型对运行环境要求较高而ONNX格式就像是个万能翻译器能让模型在各种推理引擎上畅通无阻。我去年在部署ChatGlm2到边缘设备时就深有体会。客户现场用的是华为昇腾芯片原生不支持PyTorch。当时花了三天时间折腾环境最后还是靠转ONNX才解决问题。转成ONNX后不仅摆脱了框架依赖在Intel CPU上实测推理速度还提升了20%左右。ONNXOpen Neural Network Exchange的本质是种开放的模型表示格式。它就像神经网络界的PDF任何支持ONNX的推理引擎都能直接运行模型。目前从TensorRT到OpenVINO从ARM NN到ONNX Runtime主流推理框架基本都支持ONNX。这意味着你的模型可以一次导出到处部署。不过现有方案有个明显痛点大多数教程都需要修改transformers库源码。就像我同事小王上周的遭遇他按照某篇知名教程修改了transformers的LlamaModel类结果三个月后库版本升级所有修改全部失效项目差点延期。这种侵入式修改就像在别人家墙上凿洞既破坏原有结构又难以维护。2. 零侵入导出方案的核心思路经过两个月的踩坑实践我总结出一套无需修改transformers源码的导出方法。核心思路就像用适配器而不是改电路——通过包装原始模型来满足导出需求。具体来说我们创建了一个轻量级的ModelWrapper类它继承自原始模型类但重写了forward方法。以ChatGlm2为例原始模型的forward需要处理复杂的attention_mask计算。我们在wrapper里将其简化为直接接收处理好的expanded_mask作为输入。这相当于把预处理工作挪到模型外部既保持了原始模型完整性又简化了ONNX图结构。实测显示这种处理能让导出的ONNX模型减少约30%的冗余节点。class ChatGlm2Wrapper(ChatGlm2PreTrainedModel): def __init__(self, model): super().__init__(model.config) self.model model def forward(self, input_ids, position_ids, attention_mask): # 简化版的forward逻辑 outputs self.model( input_idsinput_ids, position_idsposition_ids, attention_maskattention_mask ) return outputs这套方案最大的优势是兼容性。我在transformers 4.28到4.36多个版本上测试都能正常工作完全不用担心库升级导致导出失败。对于不熟悉模型内部结构的开发者也很友好你只需要知道模型输入输出的shape和dtype即可。3. 具体导出步骤详解现在让我们手把手完成整个导出流程。以导出ChatGlm2-6B为例你需要准备至少16GB内存的Linux服务器实测14B模型需要32GB安装torch 1.12和transformers库提前下载好模型权重首先加载原始模型并创建wrapper实例python -c from transformers import AutoModel from wrapper import ChatGlm2Wrapper model AutoModel.from_pretrained(THUDM/chatglm2-6b, trust_remote_codeTrue) wrapped_model ChatGlm2Wrapper(model).eval() 接下来是关键的导出环节。这里有个重要技巧使用operator_export_typetorch.onnx.OperatorExportTypes.ONNX_ATEN_FALLBACK可以避免某些算子导出失败。导出命令如下torch.onnx.export( wrapped_model, (input_ids, position_ids, attention_mask), # 示例输入 chatglm2.onnx, input_names[input_ids, position_ids, attention_mask], output_names[logits], dynamic_axes{ input_ids: {0: batch, 1: seq_len}, position_ids: {0: batch, 1: seq_len}, attention_mask: {0: batch, 1: seq_len}, logits: {0: batch, 1: seq_len} }, opset_version14, operator_export_typetorch.onnx.OperatorExportTypes.ONNX_ATEN_FALLBACK )导出的ONNX模型通常包含大量冗余算子。这时就需要用到我的另一个神器onnxsim_large_model。普通onnxsim处理大模型容易OOM这个改进版专门针对2GB以上的模型python -m onnxsim_large_model chatglm2.onnx chatglm2-sim.onnx \ --skip-optimization \ --skip-fuse-bn \ --input-shape input_ids:1,512 position_ids:1,512 attention_mask:1,512,5124. 常见模型的特调技巧不同模型架构需要针对性的优化策略。以我处理过的三个典型模型为例LLaMA系列主要问题是decoder层存在if分支。解决方法是在导出时设置symbolic_shapetorch._C._jit_set_symbolic_shapes(True)然后在onnxsim时添加--enable-shape-inference参数这样优化器就能自动消除冗余分支。Qwen模型大量使用einops.rearrange操作会导致ONNX图异常复杂。我的方案是用基础算子替换# 替换前 context_layer rearrange(context_layer, b s h d - b s (h d)) # 替换后 b, s, h, d context_layer.shape context_layer context_layer.reshape([b, s, h*d])ChatGlm2需要注意其独特的RotaryEmbedding实现。导出时要确保position_ids的维度匹配我建议添加shape检查断言assert position_ids.shape (batch_size, seq_len), \ fposition_ids shape mismatch: {position_ids.shape}对于百川、InternLM等LLaMA变种可以直接复用LLaMA的导出方案。我在代码库里准备了适配脚本只需要修改config.json路径即可。5. 导出模型的调试与优化第一次导出失败是常态。我总结了一套调试方法论首先检查ONNX模型是否有效python -c import onnx model onnx.load(model.onnx) onnx.checker.check_model(model) print(Graph has, len(model.graph.node), nodes) 如果报错可以通过节点名定位问题。比如遇到/layers.5/attn/MatMul报错就说明第5层attention的矩阵乘法出了问题。这时可以用Netron可视化模型对比原始PyTorch模型和ONNX模型的输出差异逐步缩小导出范围比如先导出单层性能优化方面我推荐两个必做操作使用ORTONNX Runtime的图优化sess_options onnxruntime.SessionOptions() sess_options.graph_optimization_level onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL对Qwen这类模型手动融合RotaryEmbedding相关算子能减少约15%的推理延迟。6. 实际部署效果对比在Intel Xeon 6330服务器上测试ChatGlm2-6B的对比数据指标PyTorch原始模型ONNXORT优化首token延迟420ms380ms持续生成速度28 tokens/s33 tokens/s内存占用13.2GB11.8GB边缘设备上的优势更明显。在树莓派5上测试LLaMA-7BPyTorch因内存不足无法运行ONNX量化版int8能稳定以5 tokens/s的速度生成最近我还成功将Qwen-7B部署到了Jetson Orin上。关键是把模型分成多个子图用Pipeline并行处理。这套方案已经用在某医疗问答终端设备上日均处理500次查询零故障。