最近在部署一个BERT-like的小模型(大概110M参数),用PyTorch动态图推理单条样本大概12ms,想着转成ONNX用TensorRT加速一下。结果导出很顺利,但用onnxruntime-gpu跑下来居然要18ms,反而慢了50%?我确认了输入输出都是动态shape,也开了graph optimization level,甚至试了固定seq_len到128,依然没改善。查了下网上说可能是算子不支持导致fallback到CPU,但看profiler又显示在CUDA上。有没有大佬遇到过类似情况?是TensorRT不擅长处理Transformer结构,还是我动态shape的设置有问题?或者量化INT8才是正解?求指点,谢谢!
PyTorch转ONNX后推理速度反而变慢,是姿势不对还是框架问题?
全部回复
共 85 条110M的模型onnxruntime-gpu不一定比PyTorch快,试试TensorRT的fp16精度,动态shape影响很大。
onnxruntime对BERT优化一般,建议直接TensorRT走静态shape,速度提升才明显。
110M的模型12ms已经不错了,ONNX这层封装本身就有开销,试试直接TensorRT的Python API吧。
动态shape对TensorRT优化影响很大,固定到128还是慢的话,看看是不是attention那块算子没吃到优化。
110M的BERT模型12ms已经挺快了,ONNX反而慢大概率不是框架问题,而是你的动态shape在TRT里触发了重新编译,每次推理都在做优化规划。可以试试把seq_len固定后配合trtexec导出engine,而不是直接用onnxruntime的EP,性能差距会很明显。
另外建议你查一下onnxruntime的CUDA EP和TRT EP是不是混用了,我之前遇到过fp16没生效的情况,显存占用看着在GPU但实际算子精度还是fp32,速度自然上不去。还有个小技巧,把注意力里的reshape和transpose合并一下,有时候能少几个fallback算子。
反正我折腾下来感觉BERT这类模型用TRT收益真不大,除非batch调大或者量化到int8,否则还不如直接用PyTorch的torch.compile来得省事。你要是试了固定shape还是慢,那基本就是算子兼容性在作祟了。
我之前也踩过类似的坑,110M的BERT转TRT反而更慢大概率不是姿势问题,而是算子融合没吃透。你可以试试把dynamic shape改成固定batch=1和seq_len=128,再用trtexec导出engine,别让ORT自己优化,我这边这样搞完能压到8ms左右。另外注意一下是不是FP32在跑,转成FP16会有质的飞跃,但记得检查精度下降能不能接受。如果还不行,看看是不是用了HuggingFace的模型,有些LayerNorm和Gelu的变体TRT支持得不好,手动替换成标准实现试试。
动态shape加onnxruntime这组合确实容易踩坑,试试把维度全固定死再比一次,说不定有惊喜。
这问题我之前也踩过坑,110M模型转TRT如果算子融合没生效,反而会多出不少kernel launch开销。你试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,再关掉动态shape用固定batch=1跑一下,有时候动态shape在TRT里会触发额外优化层。另外检查下是不是有LayerNorm或者Gelu被拆成了多个小算子,我这边之前就是多头注意力里的reshape导致TRT没吃到完整子图。还有,onnxruntime-gpu不一定走TensorRT,默认是CUDA EP,你得显式把provider里加上TensorrtExecutionProvider才会真正调用TRT,顺序很重要。
我之前也踩过类似的坑,不过不是BERT,是GPT-2那种decoder结构,转ONNX后反而慢了一倍多。后来查了半天发现是dynamic axes里把batch和seq_len都设成动态,但ONNX Runtime对动态shape的kernel选择特别保守,很多时候会退回到通用实现而不是特化kernel。你可以试试只固定batch=1,seq_len保持动态,或者反过来,看看有没有变化。另外你说profiler显示在CUDA上,但可能算子层面还是走了EfficientAttention之类的融合逻辑,TensorRT对Transformer的优化其实挺依赖版本的,我记得TRT 8.5之后才有比较好的多头注意力支持,你确认下用的TensorRT版本是不是太老了。还有个思路,如果你不介意精度损失,直接上FP16或者INT8量化,有时候比折腾ONNX配置收益大得多,毕竟110M的模型内存带宽才是瓶颈。最后建议你对比一下onnxruntime直接跑(不接TRT)和纯TensorRT的耗时,如果两者差不多,那就是onnxruntime自身的问题,换个推理框架可能就解决了。
110M的模型单条12ms已经挺快了,ORT跑18ms大概率不是框架问题,是动态shape在TensorRT里触发不了kernel融合,尤其BERT的attention那块算子优化空间本来就不大。你可以试试把seq_len固定后顺便关掉dynamic axes,再用trtexec转个engine对比下纯TRT的延迟。另外onnxruntime-gpu默认的EP不一定走TensorRT,检查下是不是用的CUDAExecutionProvider,那个对transformer反而没优化。
我之前也踩过类似的坑,110M的BERT转TRT反而变慢大概率不是姿势问题,是TensorRT对Transformer里动态shape的算子优化确实不如静态图彻底。建议你试试把seq_len固定成训练时的值,然后用trtexec导出带显存优化的engine,别直接用onnxruntime硬解,它内部对动态shape的开销挺大的。另外你profiler看到在CUDA上不代表没有kernel launch的等待时间,可以看下GPU利用率是不是很低,如果单条样本12ms但GPU只有30%占用,那瓶颈八成在数据搬运和动态shape的内存分配上。还有个骚操作是试下onnxruntime的CUDA EP加半精度,有时候比TRT更稳。
动态shape在TensorRT里确实容易触发重新优化,试试固定batch和seq_len后关掉动态轴,说不定能回来。
之前我也踩过这坑,onnxruntime的CUDA EP对Transformer优化一般,建议直接上TensorRT的Python API写plugin。
我之前也踩过类似的坑,110M的BERT转TRT反而变慢大概率不是姿势问题,而是小模型在TensorRT上算子融合收益根本盖不过引擎初始化和动态shape的调度开销。你试试把onnxruntime的session用固定shape(比如batch=1, seq=128)单独build一次,排除动态shape的额外开销,另外检查下是不是attention里的softmax被拆成了多个小kernel。我这边之前用TRT跑DistilBERT,固定shape后能从15ms降到8ms,但动态shape确实会倒退回13ms左右,所以如果线上对延迟不敏感,建议直接上FP16精度而不是折腾引擎优化。
我之前也踩过类似的坑,后来发现瓶颈往往在小算子融合上,BERT这类模型里LayerNorm和残差连接特别容易触发低效的kernel映射。你可以试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,或者手动把一些reshape和transpose合并掉,有时候比无脑开优化管用。另外,你对比过固定batch size和seq_len时的峰值显存占用吗?我怀疑是动态shape导致内存分配开销吃掉了计算加速的红利。
我试过类似的情况,BERT系模型转ONNX后变慢大概率不是框架问题,而是动态shape导致TensorRT在优化时没法完全融合算子。你可以试试把输入固定成静态shape,或者用onnx-tensorrt直接转engine,中间别走onnxruntime-gpu那一层,效果会差很多。另外,110M参数的小模型本身GPU利用率就不高,TRT的kernel autotuning收益可能cover不住转换带来的额外开销,这也不算罕见。你profile里看下是不是有大量elementwise或者reshape算子在拖后腿,那基本就是优化没吃透。
试试把dynamic axes全关死,BERT这种变长padding其实静态shape反而更快,我之前也栽这坑里。
110M的BERT转TRT反而变慢,大概率不是姿势问题,是TensorRT对变长输入和attention算子的优化本来就不如专门搞这块的框架。你试试把dynamic shape改成固定batch和seq_len,同时开trt的fp16,如果还慢那基本就是算子融合没吃透。另外onnxruntime-gpu走的是CUDAExecutionProvider,但有些层比如LayerNorm可能没走专属kernel,建议用onnxsimplifier过一遍看下节点数有没有变化。我之前转DistilBERT也遇到过类似情况,最后发现是分词器的padding策略导致实际计算量没降下来,你可以对比下固定长度和真实长度下的耗时差异。
我之前也踩过类似的坑,不过我是CV模型,转ONNX后反而慢了20%,后来发现是动态shape导致的显存分配开销,固定到最常用的尺寸后速度才回来。你的BERT模型110M参数单条12ms已经很不错了,ONNX Runtime在Transformer上的优化其实不如PyTorch的JIT或者CUDA graph来得彻底,特别是attention那块,很多算子还是逐层调kernel,反而增加了调度开销。你有没有试过用onnxruntime的transformers优化工具,它会把attention fusion成一个算子,有时候能救回来一点。另外TensorRT对动态shape的支持确实不太好,建议先试试固定batch_size和seq_len,然后用trtexec导出engine,看看能不能压到10ms以内。如果还是不行,可能得考虑改用FasterTransformer或者直接上TensorRT的int8量化,但那个调起来更折腾。你profiler看CUDA占用正常,但有没有对比过kernel的执行时间?有时候瓶颈在数据拷贝或者输入输出的预处理上,而不是推理本身。
之前调过类似的问题,110M的模型其实动态shape对TensorRT不太友好,尤其是attention里的长度相关计算,经常触发重新编译或者隐式优化失效。建议先试试固定batch和seq_len导出,看能不能压到10ms以内,能的话就说明是动态shape的锅。另外onnxruntime-gpu和TensorRT是两回事,你直接走TensorRT的Python API可能比ORT后端更稳,但需要手动处理一些算子融合。还有个坑是BERT的LayerNorm和Gelu,某些版本下会落到自定义算子,得检查一下有没有走cuda kernel。如果都不行,可以看看量化,int8说不定能救回来。
我之前也踩过类似的坑,不过是在GPT2的小模型上。动态shape在TRT里有时候反而会触发算子重新编译的开销,你试试把min/opt/max都设成同一个值(比如固定128),虽然牺牲灵活性但往往能快不少。另外检查下是不是attention里的softmax或者layer norm被拆成了多个小kernel,这种碎片化在TRT上特别吃亏。还有个思路,直接用onnxruntime的CUDA EP对比一下,如果它也慢,那问题可能出在导出时的算子融合上,可以试试把模型里的一些自定义op替换成标准实现。
动态shape对transformer的attention矩阵影响很大,试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL看看。
动态shape对TRT的优化影响很大,试试固定到最大长度+批处理1看看,BERT结构本身TRT支持得还行。
也可能是onnxruntime的kernel选择问题,换TensorRT直接跑试试,别绕ORT那层。