最近在部署一个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的BERT转TRT确实不一定会更快,尤其是动态shape。你可以试试把注意力头的计算单独拆出来看,有时候是reshape和transpose这些操作在TRT里被优化得很差,反而比PyTorch的cudnn还慢。另外,onnxruntime-gpu默认用的其实是CUDA EP,不是TensorRT,你确认下是不是真的走了TensorRT execution provider,这俩差别很大。我之前试过固定seq_len到64并且把batch也定死,速度才勉强跟PyTorch持平,动态shape对Transformer来说代价太高了,可以考虑换FasterTransformer或者直接上vLLM那套方案。
我之前也踩过类似的坑,110M的BERT转TRT反而变慢大概率不是姿势问题,而是算子融合没生效。你试试把onnxruntime的execution_mode设为ORT_SEQUENTIAL,或者干脆用tensorrt直接加载onnx转engine,别经过ort,这样能绕开不少fallback。另外动态shape在TRT里有时候会触发cudnn的保守选择,固定到128如果还慢,可以看看是不是attention的transpose和reshape被打散了,这结构确实不是TRT强项。你profiler显示在CUDA上但没准是kernel launch开销大了,试试把batch调大点看吞吐有没有变化,单条延迟对比其实不太公平。
110M的BERT转TRT反而变慢,我赌五毛是动态shape的锅,你试试把seq_len固定成64或者96,很多时候动态范围越大TensorRT越爱摆烂。另外onnxruntime-gpu不等于TensorRT,它默认execution provider是CUDA,你如果没显式加trt provider,那其实还是在用ONNX Runtime自己的算子,跟TRT半毛钱关系没有。建议直接用trtexec转个engine看看真实延迟,顺便检查下是不是有LayerNorm或者Gelu被拆成一堆小算子,这种在小模型上特别容易因为kernel launch overhead反超。
动态shape对TensorRT影响挺大的,试试固定到最长序列再量化下,说不定能反超。
110M的模型单条12ms其实已经挺快了,转ONNX后反而慢多半是动态shape的锅,TensorRT对动态维度会做很多额外的内存管理和算子融合优化,性能反而容易退化。你试试把seq_len固定后顺便把batch也固定成1,然后用trtexec单独导出engine文件,别用onnxruntime直接加载,这样能绕开它自带的图优化开销。另外BERT里的LayerNorm和GELU在TRT老版本上确实容易走fallback,虽然profiler显示在CUDA但可能是不同的kernel实现,建议升级到TRT8.6以上并开fp16,我这边之前有个类似模型从20ms优化到7ms。
其实你这个情况我遇到过,110M的BERT转TRT以后慢半拍挺常见的,尤其是动态shape没锁死的时候,TensorRT会对每个shape都重新优化一遍,开销反而比PyTorch的动态图还大。建议先试试把seq_len固定到64或者32跑一下,如果速度上来了那就是动态shape的锅,另外检查下是不是转的时候把attention mask或者position ids这些输入给优化掉了,有时候onnxruntime会自作主张精简图,导致实际跑的时候多了一次内存拷贝。我之前还有个经验是直接用torch_tensorrt的Python接口从PyTorch模型转,比走ONNX中间层少一次图优化损耗,你可以对比试试看。
试试固定seq_len后把opset调到17以上,另外检查下attention mask是不是被当成常量折叠了。
动态shape对TRT优化影响挺大的,试试固定batch和seq_len再用TensorRT直接转,别绕ONNX。
我之前也踩过这个坑,不过是在更小的模型上。110M参数转ONNX后反而慢,大概率不是TensorRT不擅长Transformer,而是你动态shape和算子融合没吃到红利。ONNX Runtime默认的CUDA EP对Transformer的支持其实挺拉的,很多小算子会拆得很碎,反而比PyTorch的fused kernel更慢。你可以试试把opset版本调高到17以上,然后显式关掉动态shape,用固定batch和seq_len重新导出,有些场景下固定shape能让TensorRT走更激进的kernel fusion。另外,如果profiler显示在CUDA上但时间还是长,留意下是不是有数据拷贝的隐式开销,比如输入从CPU到GPU的同步,或者输出又拷回CPU,这种小模型单条样本的延迟很容易被拷贝时间淹没。我之前是把输入改成numpy连续内存再喂给session,能快个10%左右。还有个思路是直接用TensorRT的Python API从头建engine,绕开ONNX这个中间层,虽然麻烦但针对BERT-like结构能手动指定一些plugin,效果会明显很多。你那个18ms是用onnxruntime-gpu测的,但ONNX Runtime的CUDA EP和TensorRT EP是两回事,如果你只是在ORT里选了CUDA provider,那其实没跑到TensorRT,得明确用trt provider才行。这点容易忽略,我当初就栽在这。
之前跑bert的时候也踩过类似的坑,onnxruntime-gpu对动态shape的处理确实会引入额外开销,尤其attention mask那块容易触发算子融合失败。你试试把seq_len固定后把reshape和gather相关的图优化选项全打开,或者直接转成fp16看看带宽瓶颈有没有缓解。另外TensorRT对transformer支持其实还行,但110M参数这个量级可能还没到能抵消转换损耗的程度,建议对比一下trtexec的benchmark结果再下结论。
试试把onnxruntime的execution_mode设为ORT_SEQUENTIAL,或者检查下是否走了TRT EP而不是默认的CPU EP。
110M的BERT转TRT本来就不太容易吃到红利,这个体量下算子融合省的时间可能还没动态shape带来的额外开销大。你试过把onnxruntime的execution mode设成ORT_SEQUENTIAL吗?有时候并行执行反而会让GPU频繁切换kernel。另外建议看看是不是LayerNorm或者gelu被拆成了多个小算子,这种在TRT里特别容易触发kernel launch瓶颈,手动合一下能快不少。
我之前也踩过类似的坑,BERT转TRT反而变慢大概率不是姿势问题,是TensorRT对动态shape和Transformer里某些算子的融合策略比较保守,尤其当seq_len固定后内存分配和kernel选择不匹配时更明显。建议试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,或者直接对比一下TRT的静态shape版本,如果静态能快很多那基本就锁定动态shape的代价了。另外你确认一下onnxruntime是不是真的调用了TensorRT EP,有时候provider没配好会悄悄用CUDA EP跑,那速度自然上不去。我上次是换成TensorRT直接跑onnx才解决的,但调优过程挺磨人,得慢慢试。
我之前也踩过类似的坑,而且踩得还挺深。你那个profiler显示在CUDA上,但实际很可能是有部分算子走了CUDA上的fallback实现,比如某些动态shape的gather或者attention里的reshape,TensorRT对这类操作优化很激进,但ORT反而会因为要处理动态shape而额外做很多内存拷贝和同步开销。我自己的经验是,先把模型里能固定的维度全部固定死,包括batch size也设成1,然后看ONNX导出时有没有warning说某个op不支持,有时候就算能导出,算子版本太新也会让ORT走得特别保守。你试过用TensorRT直接跑ONNX而不是用ORT的TensorRT EP吗?这两个路径差异很大,ORT那个EP其实还是经过了一层转换,性能损失可能就出在这里。另外110M参数对Transformer来说不算小,如果是12ms的基线,那大概率是CPU上的batch=1,转到GPU反而吃不满,你可以试试batch size加大到8或者16,吞吐量可能就反超了。还有个小技巧,把attention里的softmax换成带mask的固定写法,有些算子融合就吃这一套,我上次就是这么把延迟从17ms拉回10ms的。你那个动态seq_len其实很可疑,建议先用固定128导出再比较一次,如果还是慢,那就真的要考虑是不是ORT对某些层做了不必要的同步了。
我之前也踩过类似的坑,BERT类模型转ONNX反而变慢太常见了。你这110M参数不算大,但Transformer结构里的多头注意力在动态shape下特别容易触发算子融合失败,onnxruntime-gpu虽然显示在CUDA上,但可能是走了cuda kernel但没吃到TensorRT的优化红利。建议你直接试一下把dynamic axes全去掉,固定batch和序列长度,用trtexec单独导出TensorRT引擎,别让onnxruntime去自动转换,这样效率往往差很多。另外检查一下你用的onnxruntime版本,新版本对Transformer的fused attention支持差别很大,1.16和1.17在我机器上性能能差出30%。还有个思路是量化,int8对这类模型提升很夸张,但你要注意看下算子是否支持,不然精度掉了速度也没起来更头疼。我最后是直接放弃ONNX,用PyTorch的torch.compile加CUDA graph,反而跑到8ms,有时候框架自己的优化比跨框架转换靠谱多了。
我试过类似情况,BERT系模型转ONNX经常这样,尤其是attention里的reshape和transpose,TensorRT优化得并不好。你固定了seq_len还是慢,那大概率是某些算子被拆得太碎,导致kernel launch开销压不住了。可以试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,或者直接对比一下trtexec导出的engine速度和onnxruntime的差异,有时候问题出在ORT自己身上。另外你profiler看到在CUDA上不代表没有CPU fallback,有些混合执行是看不出来的,建议加上日志看下有没有警告。
110M的模型12ms已经很快了,ONNX那层封装开销可能比收益还大,试试直接TensorRT的PythonAPI吧。
动态shape在TRT里本来就有额外开销,固定到128反而可能触发算子融合失效,建议用trtexec测下真实耗时。
110M的模型12ms已经不错了,ONNX转TRT对BERT优化有限,建议直接试TensorRT原生API或FP16。
你这情况我太熟了,之前部署过类似规模的模型,ONNX导出看着没问题,但跑起来就是比PyTorch慢。个人感觉你大概率不是姿势问题,是TensorRT对Transformer里那些动态shape的算子优化得不够激进,尤其是attention那块,转成TRT后反而容易产生额外的reshape和transpose开销。我建议你换个思路,别直接用onnxruntime,试试把ONNX转成TensorRT的engine文件再跑,跳过ORT那层封装,有时候能差出一倍时间。另外你提到profiler显示在CUDA上,那可能不是fallback,更像是有算子被拆成了多个小kernel,导致启动开销占比变大,小模型尤其明显。还有个坑,动态shape的优化要配合trt的workspace设置,默认值太小会限制它做kernel fusion,你可以试试把max_workspace_size调大几个G。最后想问下,你测的12ms是纯推理时间还是包含了tokenize?如果包含了预处理,那对比就不太公平,得把前后处理都去掉再测一次。
试试固定seq_len再配合trt的min/max/opt三个档位,动态shape开销常常比想象中大。