最近在公司做一个小项目,需要把训练好的BERT模型部署到生产环境。一开始想用PyTorch自带的JIT trace,结果动态shape直接给我整不会了。后来转ONNX,又遇到LayerNorm和GELU算子不支持,各种改代码绕路,最后导出的模型精度还掉了0.3%。我心态有点崩,感觉自己在瞎折腾。想问问各位老哥,现在实际工业落地,如果不想用TensorRT(公司没N卡)也不想上服务端框架(太复杂),有没有比较稳的部署方案?或者是我用ONNX的方式不对?求指条明路,谢谢了!
PyTorch转JIT被坑惨了,ONNX导出也报算子错误,大佬们现在部署都用啥?
全部回复
共 68 条说实话你这情况我太熟了,BERT转ONNX就是场噩梦,LayerNorm和GELU在旧版opset里简直是玄学,新版opset又得配对应版本的onnxruntime,稍不留神就精度崩盘。我自己后来是直接放弃动态shape,把输入pad到固定长度,虽然浪费点显存但至少能跑通,精度掉0.3%大概率是算子融合时数值精度问题,试着把onnxruntime的优化级别调低或者关掉某些图优化试试,有时候默认的优化反而帮倒忙。
如果公司没有N卡,那TensorRT确实别想了,CPU部署的话我建议你试试OpenVINO,对Intel的CPU优化很猛,而且它对BERT这类Transformer有专门的适配,转换时对LayerNorm和GELU的处理比ONNX友好太多,我上次转一个中文分类模型,速度比ONNX runtime快了将近一倍,精度几乎无损。唯一麻烦的是它比较吃Intel的生态,如果你的服务器是AMD的U,那还是老老实实调ONNX吧。
还有一个思路是直接用HuggingFace的Optimum库,它内部封装了各种导出和优化流程,能自动帮你处理很多算子兼容问题,而且支持量化,int8精度掉得比fp16还少。不过说实话,如果你只是一个小项目且不想折腾,直接用TorchScript的script模式代替trace,把动态shape用torch.where或者mask的方式绕过去,反而比转ONNX省心,毕竟PyTorch自家的东西至少不会报算子错误。你现在用的onnxruntime是哪个版本?有些老版本对BERT支持特别差,建议直接上1.17以上的,算子兼容性提升了一个档次。
试试ONNX Runtime直接上DNNL后端吧,CPU上BERT优化得还行,动态shape别用trace,用TorchScript scripting稳点。
说实话你遇到的这套流程我太熟了,BERT转ONNX的坑基本全踩过一遍。GELU那个算子现在有些onnxruntime版本能支持了,但LayerNorm还是看运气,我后来是直接改模型代码把自定义算子注册进onnxruntime的C++层,虽然麻烦但精度能保住。动态shape的话,建议你trace的时候固定seq_len到128或者256,线上padding到那个长度,虽然有点浪费算力但省心。另外精度掉0.3%不一定全是算子问题,检查下onnx的优化选项,有时候图优化会折叠掉一些数值敏感的节点,关掉几个pass试试。如果你不想碰TensorRT,其实onnxruntime配合CPU推理已经挺稳了,就是慢点,但部署简单啊。还有个偏门思路,用libtorch但别用jit trace,直接写c++前向逻辑,动态shape完全可控,就是代码量翻倍。最后提醒下,量化这块如果精度敏感,先别碰int8,fp16在CPU上也没啥意义,老老实实用fp32跑吧。
试试转成OpenVINO吧,CPU上优化好还省心,就是得先修好算子再导出。
ONNX坑确实多,要不直接上FastTransformer或者自己写C++推理,虽然累但可控。
ONNX那个算子报错我也踩过,GELU多半是版本太旧,换个onnxruntime版本或者用onnxsim简化一下图能省不少事。动态shape的话JIT确实不行,但ONNX可以设dynamic_axes,你试过没?精度掉0.3%可能是导出时把某些层折叠了,检查下opset版本和优化选项。要是公司没N卡,其实可以先试试ONNX Runtime配CPU的int8量化,速度提升挺明显的,不一定非要上重型框架。
ONNX坑多,建议直接上OpenVINO,CPU部署BERT稳得很,精度损失也小。
ONNX那个坑我也踩过,GELU和LayerNorm真的是老难题,后来我发现直接锁定onnxruntime的版本,再配合onnxsim去简化图,能省掉不少算子兼容的麻烦。精度掉0.3%的话,建议你先检查一下导出的模型是不是在FP16下跑的,或者试试把动态轴固定成最大长度,很多情况下精度损失是输入padding方式变了导致的。真要图省事,其实用CTranslate2或者OpenVINO也行,前者对BERT支持很到位,后者不用N卡也能跑得很欢,就是得花点时间调一下线程数。
试试ONNX Runtime的DNNL和OpenVINO EP吧,CPU上跑BERT稳得很,算子兼容性比裸导出强多了。
说实话你遇到的这些坑我也都踩过,动态shape问题用JIT trace基本无解,ONNX对BERT这种结构确实不友好。我后来是直接用ONNX Runtime但把模型拆成几个子图分别导出,再手动拼起来,精度问题多半出在GELU近似实现上,换个精确版本能救回来。不过要是图省事,现在很多团队直接上FastTransformer或者自己写C++推理,虽然前期麻烦但后面真的一劳永逸。你公司没N卡的话,试试Intel的OpenVINO?对Transformer优化挺到位的,就是环境配置也有点烦。
说实话你这个问题太典型了,BERT转ONNX的算子坑基本绕不开,GELU和LayerNorm在不同版本里实现差异很大,建议直接锁死onnxruntime的版本,然后用onnxsim简化图再试一次。精度掉0.3%大概率是动态shape导致某些层被重算,试试把序列长度固定到最大,padding掉多余部分,很多算子错误能直接消失。如果实在不想折腾,可以看看FastTransformer或者LightSeq,纯CPU上跑BERT也能快不少,就是得自己改改前处理。
这题我太熟了,之前也卡在ONNX的LayerNorm上,后来直接换成了ONNX Runtime的扩展算子,精度问题多半是融合算子没匹配对。你要是非N卡环境,试试Intel的OpenVINO吧,对BERT支持挺全的,动态shape也友好,改起来比ONNX省心不少。另外你精度掉0.3%,检查下是不是导出时把attention mask给常量折叠了,那玩意儿影响很大。
说实话你遇到的那几个坑我都踩过,LayerNorm和GELU在ONNX里确实容易出幺蛾子,但0.3%的精度掉更可能是导出时某些op被替换成低精度实现导致的。建议你先试试把动态shape固定成batch=1加padding到最大长度,很多算子错误直接消失;另外检查下ONNX opset版本,11和13的行为差别挺大的。如果还不行,可以看看Intel的OpenVINO,对BERT优化做得不错,CPU上速度比ONNX Runtime默认还快一截,而且不需要改模型结构。
另外你提到公司没N卡,那CPU部署的话其实可以考虑直接用ONNX Runtime的int8量化,配合dynamic shape支持,虽然要自己写点预处理但比JIT省心多了。我最近在搞一个项目也是类似情况,最后是用ONNX Runtime加transformers的onnx导出脚本,把GELU手动换成近似实现,精度没掉,速度也够用。别太迷信TensorRT,很多时候绕开它反而更稳。
ONNX那套对BERT系模型确实不友好,LayerNorm和GELU在不同版本的opset里经常出幺蛾子,精度掉0.3%大概率是算子融合方式变了。建议试试把模型转成ONNX后用onnxruntime的CPU推理,同时把动态轴固定到最大长度,或者用CTranslate2,它对Transformer支持很完善,CPU上速度比ONNX还快不少,而且自带量化。
ONNX转的时候把dynamic_axes配好,再加onnxsim精简一下,精度问题多半是算子融合的锅。
ONNX那个精度掉0.3%大概率不是算子问题,是动静态shape转换时某些数值被重算导致的,可以先试试把dynamic_axes全关掉,输入pad成固定长度再导出,很多坑能避开。没N卡的话,其实可以考虑用OpenVINO,对CPU优化很猛,而且对Transformer的支持比ONNX Runtime好不少,LayerNorm和GELU都是原生算子,不用改代码。另外你JIT trace动态shape搞不定的话,可以试试TorchScript的script模式,虽然写起来麻烦点,但比trace稳,至少不会静默出错。
说实话你这情况我太懂了,上个月刚踩完一模一样的坑。ONNX导出报错很多时候是版本问题,transformers库跟onnxruntime版本对不上就会各种幺蛾子,建议pin死版本再试一次。真要图省事,直接部署onnxruntime试试,但记得用onnxruntime-extensions注册自定义算子,LayerNorm和GELU都有现成的实现,精度掉那0.3%可能是你量化或者混合精度没调好。当然,如果模型不大,直接上C++用libtorch重写推理,反而省心,毕竟原生态支持动态shape。
我看你绕了半天其实核心是动态shape和算子兼容,这俩在CPU上都有解。首先试试Paddle Inference?它家对BERT类模型做了专门优化,
看到你这经历我太有共鸣了,之前我们做NLP服务也卡在GELU和LayerNorm上,ONNX那几个自定义算子折腾得人想摔键盘。后来我们直接换了个思路,用ONNX Runtime的C++接口配合动态shape输入,虽然还是有点限制,但至少不用自己写算子,精度也保住了。你掉那0.3%我猜大概率是导出时把某些融合优化关了,或者opset版本没选对,建议试试opset 17以上,有些新算子支持会好很多。如果服务端能接受Python的话,其实可以试试FastAPI加ONNX Runtime的异步推理,部署简单,并发也够用,前提是你们对延迟要求不是特别变态。另外我好奇你们有没有试过把模型量化一下,比如用INT8,有时候精度损失反而比算子绕路小,而且CPU上提速明显。反正别死磕JIT了,那东西对动态图简直就是灾难,能用静态图解决的就别跟它较劲。
试试转成ONNX时把dynamic_axes配好,或者直接上OpenVINO,CPU部署BERT稳得很,精度损失也小。
说实话你这套组合拳我太熟了,之前搞distilbert也差点被ONNX的opset版本坑到怀疑人生。动态shape这块建议直接锁死opset 14以上,然后GELU换成tanh近似能省不少事,精度掉0.3%大概率是LayerNorm的epsilon被量化吃掉了。如果公司没有N卡,可以考虑用Intel的OpenVINO,对Transformer优化得很狠,CPU上比原版ONNX runtime快两倍多,而且算子兼容性比ONNX好一截。另外就是别死磕trace,试试TorchScript的script模式配合torch.jit.QuantizedLinear,虽然前期麻烦点但后期省心。
说实话你这情况我太熟了,BERT转ONNX简直就是部署界的吞金兽,LayerNorm和GELU在不同版本里实现五花八门,我上次为了修一个ReduceMean的算子差异直接熬到凌晨三点。动态shape这块,JIT确实拉胯,但ONNX也不是银弹,你精度掉0.3%很可能是把某些融合操作拆开了,或者用了opset版本太老导致数值行为变化。如果公司没有N卡,我个人建议直接试试ONNX Runtime的CPU版,配合动态轴设置,性能虽然比不上TensorRT但胜在省心,特别是最新版本对Transformer的优化已经不错了。另外你可以查一下是不是导出的模型里带了一些训练专用节点,比如dropout的training标志没关干净,这玩意儿经常悄悄影响精度。实在不行就退回PyTorch原生服务化,用TorchServe或者干脆Flask包一层,反正CPU部署也不会要求极致延迟,稳定比性能重要。你用的transformers库是几版本?换个老版本有时候反而能避开新引入的算子坑。
试试Rust推理框架吧,或者直接上C++ LibTorch,动态shape这块比JIT省心不少。
ONNX精度掉0.3%大概率是算子融合问题,换个导出版本或者用onnxsim简化下试试。