最近在公司做一个小项目,需要把训练好的BERT模型部署到生产环境。一开始想用PyTorch自带的JIT trace,结果动态shape直接给我整不会了。后来转ONNX,又遇到LayerNorm和GELU算子不支持,各种改代码绕路,最后导出的模型精度还掉了0.3%。我心态有点崩,感觉自己在瞎折腾。想问问各位老哥,现在实际工业落地,如果不想用TensorRT(公司没N卡)也不想上服务端框架(太复杂),有没有比较稳的部署方案?或者是我用ONNX的方式不对?求指条明路,谢谢了!
PyTorch转JIT被坑惨了,ONNX导出也报算子错误,大佬们现在部署都用啥?
全部回复
共 68 条说实话你这个问题太典型了,我上个月也被JIT的dynamic shape折磨过,最后干脆固定输入长度训了个专用模型才省心。ONNX那个LayerNorm报错我建议直接升到opset 17以上,很多算子兼容性会好很多,精度掉0.3%大概率是导出时把某些融合优化关了。如果实在不想碰TensorRT,试试OpenVINO吧,对BERT这类Transformer的优化做得挺到位,CPU上跑起来比ONNX Runtime还快一截。
说实话BERT转ONNX这块我也踩过坑,GELU用近似公式替换后精度掉0.3%其实算正常范围,关键看下游任务能不能接受。你现在如果不想上重型框架,可以试试把动态shape固定成最大长度+padding,这样JIT trace基本能过,推理时再配合mask,很多场景够用了。
另外ONNX算子报错的话,可以看看torch.onnx.export的opset版本,调到11以上很多LayerNorm的兼容问题能解决。实在不行就导出成torchscript后自己写个C++推理服务,几百行代码搞定,比折腾ONNX省心。
不过你公司没N卡的话,CPU部署用Intel的OpenVINO优化ONNX模型也挺香的,速度能提不少,而且对BERT支持做得挺好。
说实话你这个问题太典型了,BERT导出ONNX基本都会卡在LayerNorm上,特别是老版本torch的算子映射本来就不全。我建议你试试把模型里的GELU换成近似版本,或者直接用onnxruntime的transformers优化,能省不少事。另外动态shape这块,如果业务上允许,干脆固定到128或者256的序列长度,用onnxruntime的dynamic_axes别乱开,精度掉0.3%大概率是算子融合时数值精度问题,试试用float16导出然后CPU上跑,反而可能更稳。
说实话你遇到的这几个坑我都踩过,尤其是LayerNorm在ONNX里那堆版本兼容问题,改到怀疑人生。JIT trace对动态shape确实不友好,但如果你能接受固定序列长度(比如padding到128),其实也能凑合用,关键看业务能不能妥协。至于精度掉0.3%,我怀疑不是算子问题,而是导出时某些融合优化把数值精度改了,你可以试试关掉ONNX的优化pass,或者用opset 17以上版本再看看。如果公司没N卡又不想上服务端框架,我建议你直接考虑ONNX Runtime + 动态轴,它现在对BERT系列支持挺好了,很多坑官方都填了。实在不行还有个土办法,就是转成TorchScript后自己写个简单的C++推理服务,绕开ONNX那层,但维护成本你得想清楚。对了,你试过用Intel的OpenVINO吗?它对Transformer模型优化挺狠的,CPU上跑BERT比ONNX Runtime还能快个20%左右,而且支持动态shape,就是环境配置稍微麻烦点。最后想问下,你那个动态shape是输入长度变化大还是batch大小变化大?这两个处理思路不太一样,搞清楚之后选型会容易很多。
说到这个我太有共鸣了,我上个月刚把一个小型DistilBERT搬到CPU服务器上,JIT trace那个动态序列长度的问题直接让我加班到凌晨两点。ONNX那块我倒是没踩LayerNorm的坑,但GELU在旧版本opset里确实是个老大难,后来我干脆把GELU手动改写成数学公式用基础算子拼出来,精度掉了0.1%以内还能接受。
试试转成ONNX时把动态轴固定住,或者用onnxruntime的transform优化下,精度掉0.3可能是某些算子在fp16下的问题。
试试把动态轴固定成最大长度+padding,精度0.3%大概率是GELU近似误差,换tanh近似能救回来。
试试ONNX Runtime配onnxsim,把动态轴固定死再优化下算子,精度掉那点基本能追回来。
这波我太懂了,当初我搞ALBERT也是被JIT的动态shape折磨到怀疑人生。ONNX那个LayerNorm报错其实可以试试把opset版本拉到17以上,有时候能白嫖到新算子支持。精度掉0.3%大概率是GELU近似实现的问题,建议导出前把模型里的gelu换成tanh近似版,跟ONNX默认行为能对齐。如果不想碰TensorRT,要不看看OpenVINO?对CPU优化很猛,而且直接吃ONNX模型,不用改代码。
onnxruntime现在对bert支持挺成熟了,你试试把LayerNorm和GELU合到attention里导出,精度掉0.3大概率是动态轴没锁死。
ONNX那个LayerNorm报错我熟,多半是opset版本太老,换到13以上能省掉一半折腾。另外动态shape别硬刚,固定成128或者256,线上padding一下真没差多少精度。0.3%的掉点我倒觉得可能是量化或者算子融合搞出来的,先纯fp32导出对比下。你要是CPU部署,其实OpenVINO对BERT支持挺省心的,转起来比ONNX顺滑,要不试试?
说实话你这个问题太典型了,BERT转ONNX最坑的就是那些LayerNorm和GELU的变体,不同版本PyTorch导出的算子兼容性都不一样。我建议你试试把模型固定成静态shape,或者直接用transformers库自带的ONNX导出脚本,它会把很多特殊算子处理成可用的组合。另外精度掉0.3%大概率是fp16或者某些融合优化导致的,可以先强制fp32排查。如果实在不想折腾,Rust + Candle或者ONNX Runtime的DNNL后端都挺稳的,至少比JIT省心。
你这情况我也踩过,动态shape直接上RapidJSON或FastAPI自己包个推理服务最省心,别死磕ONNX。
其实你精度掉0.3%大概率是ONNX的LayerNorm融合问题,试试把opset调到17以上能省很多事。
试试把动态轴固定成最大长度+padding,精度掉0.3大概率是GELU近似实现的问题,换成自带op就好。
试试把动态轴固定成最大长度+padding,精度掉那0.3多半是GELU近似误差,换成自定义OP能救回来。
说实话你这情况我太理解了,BERT导出ONNX就是玄学,我上次搞RoBERTa也是卡在GELU上,后来用torch.onnx.export的custom_op硬写了个算子才过。不过精度掉0.3%大概率是动态shape导致op融合变了,你可以试着固定序列长度再导一次,或者直接用ONNXRuntime的CUDA EP,小模型跑CPU也够用。真要省事的话,试试HuggingFace的Optimum加ONNXRuntime,很多坑都帮你踩平了,动态轴也支持得挺好。
我也是从JIT转ONNX一路踩过来的,BERT这种动态shape确实无解,建议直接锁定sequence length+padding,牺牲点性能换稳定。算子报错的话试试把GELU换成tanh近似,LayerNorm用opset 11以上版本基本能绕过去。精度掉0.3%大概率是动态轴没设对,ONNX导出时得显式标出batch和seq维度,别用默认全静态。另外可以看看ONNX Runtime的CPU版,自带图优化,比裸跑PyTorch快不少,也不依赖N卡。实在嫌麻烦就直接上FastText或者蒸馏小模型,部署省心太多了。
ONNX导出BERT确实容易卡在LayerNorm这些算子上,不过0.3%的精度掉得有点诡异,建议先查下导出时是不是把动态axis设错了,或者某些op被替换成了低精度实现。如果公司没有N卡,可以考虑用OpenVINO,对Intel CPU优化很到位,而且原生支持动态shape,导出时把opset调到最新的,很多算子问题能直接避开。另外实在不行就试试把GELU换成近似版本,比如用tanh近似,导出成功率会高不少,精度损失基本可以忽略。
说实话ONNX这块儿我踩坑比你还深,BERT导出精度掉0.3%大概率是GELU被替换成近似实现的问题,建议直接锁死onnxruntime的算子版本,或者干脆用torchscript配合libtorch,动态shape用torch.jit.script手写一下也不算太麻烦。要是嫌重,其实可以试试把模型转成ONNX后用onnx-simplifier跑一遍,很多算子兼容问题能自动修掉,精度那块儿再开一下优化选项看看能不能拉回来。