最近在公司做一个小项目,需要把训练好的BERT模型部署到生产环境。一开始想用PyTorch自带的JIT trace,结果动态shape直接给我整不会了。后来转ONNX,又遇到LayerNorm和GELU算子不支持,各种改代码绕路,最后导出的模型精度还掉了0.3%。我心态有点崩,感觉自己在瞎折腾。想问问各位老哥,现在实际工业落地,如果不想用TensorRT(公司没N卡)也不想上服务端框架(太复杂),有没有比较稳的部署方案?或者是我用ONNX的方式不对?求指条明路,谢谢了!
PyTorch转JIT被坑惨了,ONNX导出也报算子错误,大佬们现在部署都用啥?
全部回复
共 68 条试试把动态轴固定成最大长度padding,精度掉0.3大概率是GELU近似误差,换tanh近似能救回来。
试试把动态轴固定成最大长度加padding,GELU用近似公式替换,精度掉0.3大概率是LayerNorm的epsilon没对齐。
试试ONNX Runtime的dynamic axes加torch.onnx.export的opset=17,LayerNorm换aten实现能省不少事。
试试把动态shape固定成几个档位再导出,精度掉了大概率是算子熔断问题,不用TensorRT的话openvino也挺稳的。
试试OpenVINO吧,CPU上优化比ONNX Runtime狠,BERT动态shape也支持得挺好。
ONNX那个算子报错太真实了,BERT导出十次有八次卡在LayerNorm上。建议试试把模型转成ONNX时用opset 13+,然后配合onnxsim简化一下图,很多兼容性问题能少一半。精度掉0.3%可能不是算子问题,是动态shape时padding策略没对齐,你检查下导出时有没有固定sequence长度。如果不想折腾,直接上C++用libtorch跑fp16推理也挺稳,我们这边小模型就这么干的,省心。
说实话ONNX这坑我也踩过,BERT这种动态长度的模型转起来就是折磨。你试试把input的seq_len固定成最大长度,然后导出时用onnx-simplifier过一遍,精度掉0.3%大概率是GELU近似实现的问题,换成精确版本能救回来。部署的话如果不想上框架,可以看看OpenVINO,CPU上优化挺猛的,对Transformer支持也比ONNX Runtime省心。
说实话你这条弯路我太熟了,当年我搞bert蒸馏也是从jit trace开始,动态seq_len直接崩到怀疑人生。ONNX那个gelu报错其实有个土办法,就是自己写个torch.autograd.Function包一下,但精度掉0.3%大概率不是算子问题,而是你导出的模型里某些op被融合后数值计算顺序变了,尤其fp16下更明显。
既然你不想碰TensorRT和重型服务框架,我建议试试ONNX Runtime直接上CPU,把intra_op线程数调好,配合动态轴(symbolic shape)其实能跑,就是延迟比N卡高不少。或者更省事点,直接用torchscript的script模式而不是trace,把动态shape用@torch.jit.script标注成部分静态,能绕开不少坑。
还有个偏门思路,如果你只是生产推理不求极致性能,干脆用transformers库的pipeline直接跑,配合torch.compile(如果你用的是2.x版本),虽然启动慢点但稳定性好,减少自己手写导出层的调试成本。精度那个0.3%建议你回头检查下原始模型在fp32下的输出,有时候是onnx默认把某些norm的eps给改了,手动把它设回1e-12就行。
最后说句实在的,如果你们没有GPU部署需求,又不想上框架,那不如直接考虑用C++的libtorch加载原模型,虽然打包大点,但至少不用跟onnx的op斗智斗勇。别灰心,这坑基本人人都踩过一轮。
说实话你这情况太典型了,BERT转ONNX的坑基本都踩了一遍。建议试试把动态shape固定到最大长度然后padding,虽然浪费点显存但省心太多,算子问题也可以直接去ONNX的Github提issue,或者用onnxruntime的优化pass手动替换掉不支持的节点。
另外0.3%的精度掉损大概率不是导出问题,是量化或者动态shape引起的数值抖动,建议对比一下原始模型和导出模型的输出分布。如果公司没有N卡,其实可以考虑用Intel的OpenVINO,对Transformer支持比ONNX好不少,CPU部署性能也够用,就是文档有点乱,得耐心翻。
我之前做QA模型就是这么干的,固定到128长度,精度基本无损,延迟也就增加了十几毫秒。实在不行就退回PyTorch的torchscript,但记得用script而不是trace,把动态维度全部显式声明。
ONNX那个GELU报错我熟,换个新点的opset版本基本能解决,LayerNorm的话得手动拆成小算子或者用onnxsimplifier优化一下。精度掉0.3%大概率是动态shape导致的,试着固定输入长度或者用onnxruntime的dynamic axes重新导一遍。没有N卡的话可以看看OpenVINO,CPU上性能提升挺明显的,对BERT支持也还行。
说实话你踩的这几个坑我都踩过,BERT转ONNX的LayerNorm和GELU简直是重灾区,尤其是老版本torch导出的算子跟ONNX opset版本对不上,改起来真要命。不过精度掉0.3%我怀疑不是算子问题,而是动态shape导致某些维度被固定后,padding mask的计算方式变了,你可以检查下导出时有没有把attention mask一起trace进去。如果公司没N卡,我建议你直接试下ONNX Runtime的CPU版,配好int8动态量化,其实速度能接受,关键是别用torch的jit,那个对动态shape支持就是半残。另外有个野路子,用HuggingFace的Optimum库导出,它内部帮你处理了大部分兼容性问题,省得手写一堆条件判断。最后提醒下,ONNX导出后一定要用onnxruntime的推理结果跟原模型做逐层输出对比,别只看最终精度,不然哪一步出问题都发现不了。
同款遭遇,之前也被JIT的dynamic shape坑过,后来直接放弃trace改script写,能好一点但工作量也上去了。ONNX那个算子报错我倒没遇到,不过精度掉0.3%大概率是op融合或常量折叠搞的鬼,试试关掉一些优化项。你要是非N卡环境,其实可以考虑用ONNX Runtime的CPU版本,把线程数和内存池调好,BERT这种模型跑起来也够用。还有一招是直接用C++的libtorch,虽然重但省心,至少不用折腾格式转换。
说实话你这个经历太真实了,动态shape和自定义算子基本是绕不过去的坎,ONNX对BERT的支持确实有点拉胯。如果公司没有N卡,我建议你试试OpenVINO,对CPU推理优化很猛,而且直接支持动态shape,我上次把BERT切一半精度都没怎么掉。另外你精度掉0.3%可能跟导出时的opset版本有关,试试调到13以上,有些算子实现会更新。要是还不行,干脆用ONNX Runtime的DNNL执行提供程序,比默认CPU快不少。
ONNX那个坑我也踩过,LayerNorm用老版本opset经常炸,换成opset13以上会好很多。动态shape别硬刚,固定成128或者256,线上padding一下精度基本没损失。没N卡的话可以试试OpenVINO,对BERT优化挺多的,CPU上能快个3-5倍。
同款经历,BERT转ONNX那个LayerNorm报错我现在看到都PTSD了,GELU我们当时是手动改成了近似公式才勉强导出去。你精度掉0.3%其实还好,我们当时掉到0.8%直接被业务方打回,最后只能把导出前的模型蒸馏了一版才救回来。说个思路你参考下,如果公司没有N卡又不想上重型框架,试试把推理拆成两段,前面tokenizer和动态padding用Python处理,中间主干部分固定成静态shape导出,虽然牺牲点吞吐但稳定得多。另外ONNX算子不兼容很多时候是Opset版本太旧,你试试升到17以上,很多新算子都补了。还有个野路子,直接用C++加载PyTorch的libtorch,但推理时关掉gradient,动态shape用torch::jit::setTypeMetaToDevice来规避,比ONNX省心不少。反正别在一条路上死磕,我最后是用了ONNX Runtime的DNNL EP,CPU上速度比PyTorch原版快了两倍,精度也没再掉。
说实话你这情况我太熟了,BERT转ONNX那堆算子报错基本是必经之路,尤其GELU不同版本实现五花八门,LayerNorm又老被某些优化pass搞出精度差。我后来干脆直接用ONNX Runtime自带的高性能算子重写了一部分自定义OP,精度掉得少一点,但还是得花时间调。你要是愿意折腾,可以试试把PyTorch模型直接转成TorchScript的script模式,别用trace,动态shape用script写条件分支能保住,但得手动改不少forward逻辑。另外有个偏门点的思路,直接用ONNX Runtime的Python接口做推理,不做静态图导出,虽然性能比TensorRT差一截,但省心得多,稳定性也够。说实话0.3%的精度损失有时候是fp16或算子融合导致的,你可以在导出时把opset版本调高一点,或者关掉一些图优化选项试试。要不考虑下Intel的OpenVINO?虽然对BERT支持一般,但CPU部署优化得很猛,而且不用N卡。反正别指望一条路走到黑,多换几个runtime交叉验证下结果,找到最适合你那个模型结构的就行。
ONNX这坑我也踩过,动态shape直接锁死onnxruntime试试,精度掉0.3大概率是算子融合问题。
试试把LayerNorm和GELU手写导出,或者用torchscript加固定shape,比ONNX稳不少。
说实话你这经历我太熟了,BERT转ONNX那个LayerNorm报错基本是必经之路,我上次搞了三天最后发现是opset版本没对齐,换到11就过了。不过精度掉0.3%这个有点蹊跷,你检查过动态axis的reshape有没有被trace成固定值吗?我怀疑是这里出了问题,很多时候不是算子不支持,而是转换时把某些维度硬编码了。
如果公司没N卡,那TensorRT确实不用想,但ONNX Runtime其实挺够用的,特别是最新版本对Transformer优化了不少。你可以试试直接导出成fp16或者int8量化,精度损失反而比折腾算子更可控。另外JIT那条路也不是完全走不通,关键得用torch.jit.script而不是trace,脚本化模式对动态shape友好很多,就是得改点代码。
不过说真的,你要是不想碰服务端框架,还有个野路子是用FastAPI直接包PyTorch模型,配个Gunicorn扛并发,小项目完全够用。虽然官方不推荐生产这么干,但我见过不少公司就这么跑着,只要做好模型锁和内存管理,延迟也就多个几毫秒。你那个精度问题,我建议先排查一下是不是GELU的近似实现差异,PyTorch和ONNX的默认算法有时候不完全一样,手动指定一下误差函数可能就解决了。
说实话ONNX算子报错这块我太有同感了,BERT系模型导出基本都会踩一遍LayerNorm的坑。你试试把模型里自定义的LayerNorm换成onnx支持的版本,或者用onnxsim简化一下图,精度掉0.3%大概率是某些op被替换成近似实现导致的。另外动态shape别硬刚,固定到最大长度padding一下,部署时省心很多。
试试把动态轴固定成最大长度padding,精度掉了用onnxruntime的fp16试试,能救回来不少。