最近在公司做一个小项目,需要把训练好的BERT模型部署到生产环境。一开始想用PyTorch自带的JIT trace,结果动态shape直接给我整不会了。后来转ONNX,又遇到LayerNorm和GELU算子不支持,各种改代码绕路,最后导出的模型精度还掉了0.3%。我心态有点崩,感觉自己在瞎折腾。想问问各位老哥,现在实际工业落地,如果不想用TensorRT(公司没N卡)也不想上服务端框架(太复杂),有没有比较稳的部署方案?或者是我用ONNX的方式不对?求指条明路,谢谢了!
PyTorch转JIT被坑惨了,ONNX导出也报算子错误,大佬们现在部署都用啥?
全部回复
共 80 条ONNX这个坑我也踩过,GELU和LayerNorm确实是重灾区,后来我们直接换成了ONNX Runtime的优化算子集,精度掉0.3%大概率是某些op被替换成低精度实现导致的,你可以检查下导出时的opset版本和优化级别。如果不想折腾,其实可以试试把模型转成ONNX后直接用onnxruntime的C++接口,动态shape用symbolic shape配合dynamic axes能解决大部分问题,别用JIT那条路了。你现在的部署环境是CPU还是自研芯片?如果对延迟不敏感,直接用PyTorch的torchscript加上torch.set_grad_enabled(False)加eval模式,配合torch.compile可能都比绕ONNX省心。
跟你情况差不多,之前也被JIT的动态shape坑过,最后直接换了思路用ONNX Runtime配合动态轴,算子问题靠升级版本和改导出参数绕过去了,精度没掉那么多。你要是没N卡,其实CPU上ONNX Runtime已经挺能打了,关键是把opset调到13以上,很多新算子都支持了。要是还卡在LayerNorm,试试把模型里那些自定义实现改成PyTorch原生的,导出会顺很多。
说实话ONNX这坑我也踩过,BERT的LayerNorm报错基本是opset版本和优化器没对齐,试试把opset升到17以上然后关掉graph优化,精度掉0.3%大概率是动态shape导致某些层被折叠了。你要是图省事,直接用CTranslate2吧,专门优化transformer的,CPU上跑得飞快还支持动态shape,导出也简单。不过得注意它只支持fp32和int8,精度需要自己验证下。
试试转成TorchScript时把dynamic_axes配全,或者直接用CTranslate2,BERT支持贼稳,精度也不掉。
试试把动态shape固定成几个档位再导出,精度掉了可能是前面改算子改出问题,建议分步验证。
ONNX绕不过去就试试ONNXRuntime直接上,自带优化比瞎折腾算子强,我上次就这么救回来的。
试试把动态轴固定到最大长度,加padding和mask,精度损失能小很多,ONNX对静态shape友好。
试试ONNX Runtime的DNNL/OpenVINO后端吧,动态shape和算子兼容比直接导出稳多了。
看到这个经历太真实了,BERT转ONNX那堆算子问题我当初也踩过,GELU还好说自己拼个近似,LayerNorm才是真折磨。不过精度掉0.3%大概率不是算子问题,是动态shape导致某些维度被固定后数值路径变了,建议你试试把输入padding到固定长度再trace,虽然浪费点显存但能省一堆麻烦。另外如果你只是CPU部署,其实不用死磕ONNX,直接上Intel的OpenVINO,它对Transformer系列优化得很透,而且自带LayerNorm融合,转换脚本写个十几行就能跑。要是连OpenVINO都嫌重,还有个野路子:把模型导出成TorchScript后关掉shape校验,用torch.jit.optimize_for_inference配合固定长度假输入,虽然丑但能用。最后提醒下,如果精度敏感,转完一定要用同一批测试集对比每层输出,别只看最终指标,误差可能早就在中间层积累了。
试试ONNX Runtime自带算子优化,配动态轴输入,LayerNorm和GELU直接换官方contrib版本能省不少事。
遇到过一模一样的坑,BERT转ONNX那堆自定义算子简直噩梦,后来我直接换成了ONNX Runtime的extended opset,LayerNorm用com.microsoft域能绕过去,精度掉0.3%大概率是GELU近似实现的问题,换回原版tanh近似能好很多。另外动态shape真别硬刚JIT,用固定长度padding加mask,速度反而更稳。你现在要是图省事,试试把模型导出成TorchScript然后转成libtorch的C++接口,比ONNX省心不少,就是二进制体积大点。
其实还有个野路子,直接用Python的Flask包一层模型推理,配gunicorn多进程,小流量项目完全够用,别被那些重型框架吓住,我们有个内部工具就这么跑的,维护还简单。你精度掉了0.3%如果业务能接受,干脆忽略,有时候部署和训练本来就没法做到完全一致,关键看线上指标不是看那点数值差。
说实话你这套组合拳我太熟了,BERT转ONNX遇到GELU和LayerNorm基本是必经之劫,精度掉0.3%大概率是某些融合被拆了或者动态轴处理粗了。如果不用TensorRT,可以试试把ONNX的opset版本调高到13以上,配合onnxsim把图优化一遍,很多算子问题能自己消掉。再不行就考虑用OpenVINO,虽然对N卡不友好,但CPU上跑BERT速度其实挺能打,而且它对动态shape的支持比ONNX runtime原生强不少。还有个野路子是直接导出成TorchScript时把动态维度固定成最大长度,用padding+mask硬扛,推理速度损失能接受的话最省事。你那个精度问题建议先检查一下导出的模型有没有把注意力mask当成常量优化掉,之前我遇到过类似情况。
这坑我太熟了,BERT动态shape用JIT trace基本就是给自己上刑。ONNX那个LayerNorm报错大概率是版本对齐问题,试试把opset调到13以上,GELU手动拆成数学公式导出能绕过去,精度掉0.3%可能是某些层被过度折叠了。
不过说真的,你要是不想碰TensorRT,可以考虑用ONNX Runtime直接部署,配个DNNL或者OpenVINO的EP,CPU上跑BERT效果也挺能打的,关键是不用改模型结构。另外现在HuggingFace的Optimum库对ONNX导出做了不少优化,专门处理这些坑,建议直接拿它重新导一次试试。
说实话你这套组合拳我太熟了,BERT转ONNX那个LayerNorm报错我当初也卡了两天,最后是升级到opset 13才勉强跑通。不过精度掉0.3%有点离谱啊,得看看是不是dynamic axes没设对,或者某些op被替换成低精度实现了。没N卡的话要不试试OpenVINO?对CPU优化很猛,而且现在对Transformer支持也到位,转换时还能顺手做点图优化,比你在ONNX里硬扣算子强多了。
ONNX那个坑我也踩过,GELU和LayerNorm得自己写custom op,精度掉0.3%多半是算子融合或者动态shape映射的问题。要是CPU部署的话,试试OpenVINO吧,对BERT优化挺到位的,转换时把动态shape固定到最大长度,精度几乎无损。另外实在不想折腾框架,直接上C++ LibTorch + 静态shape,虽然丑但胜在稳。
试试把动态轴固定成最大长度+padding,算子报错多半是版本太新,ONNX opset降到13基本能消停。
试试把动态轴固定成最大长度加mask,精度差那0.3大概率是GELU近似的问题,换精确实现就好。
ONNX导出LayerNorm和GELU踩坑太正常了,我一般用torch.onnx.export时把opset调到17,再用onnxsim过一遍,基本能消掉大部分算子问题。精度掉0.3%可能是GELU被拆成近似实现导致的,试试在导出前用自定义符号注册精确版GELU。部署端可以看看ONNX Runtime,CPU推理够稳,动态shape也支持得比JIT好。不想折腾服务端框架的话,直接ORT加个FastAPI就够了。
ONNX导出LayerNorm和GELU报错基本是opset版本没选对,升到opset 14以上大部分算子都覆盖了,GELU可以用Erf分解写。精度掉0.3%大概率是导出时某些算子走了FP16或者融合逻辑变了,建议导出后拿同样的测试集跑一遍逐层对比。不用N卡的话其实ONNX Runtime就挺稳的,CPU推理加上量化也就够用了,别折腾TensorRT了。
试试torch.compile加dynamic=True,ONNX那套opset版本对不齐就是容易掉精度。
ONNX对LayerNorm支持确实捉急,试试opset调高到17,GELU用Erf手动拼一下能绕过去。