最近在部署一个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惹的祸,TensorRT对动态尺寸的优化会保守很多,算子融合和显存分配都放不开手脚。建议你试试把seq_len固定成实际部署时的最大长度,然后配合onnx-tensorrt的explicit batch,哪怕输入padding到128,通常也能反超PyTorch。另外检查下是不是有LayerNorm或者Gelu这类算子被拆成了多个小kernel,TRT对这类细粒度算子的调度开销挺明显的,可以考虑用插件合并一下。还有个思路,如果显存够的话,直接上fp16精度,带宽瓶颈下收益比静态shape还大,我这边实测能压到7ms左右。
我之前也踩过类似的坑,BERT类模型转ONNX后变慢大概率不是TensorRT不行,而是dynamic shape在作祟。你可以试试把batch和seq_len都固定死,特别是把attention mask和position id也一起固化,有时候这些辅助输入才是拖慢的元凶。另外onnxruntime-gpu默认可能会走CUDA EP的某些低效kernel,建议直接上TensorRT EP而不是ORT自带的CUDA执行提供方,差距能拉开一倍。还有个细节,如果模型里有GELU或者LayerNorm的自定义实现,ONNX导出时容易被拆成碎算子,反而比PyTorch的融合版本慢。你profiler看到在CUDA上不代表没CPU fallback,建议用nsys或者NVIDIA的NVTX看看具体每个节点的耗时分布。
110M的BERT转TRT反而变慢,我赌八成是动态shape惹的祸。TRT对Transformer里的Einsum和Gelu融合其实挺挑的,你试试把opset调到17以上,再用trtexec转一次看下层融合日志,很多算子没融合就会在CUDA上跑得比PyTorch还拉胯。另外你确定onnxruntime-gpu真的调用了TRT的EP吗?默认的CUDA EP和TRT EP性能差挺多的,别搞混了。我之前有个类似模型,固定batch和seq_len后能压到8ms,动态的话直接回到15ms,这玩意儿对shape太敏感了。
我之前也踩过这个坑,110M的BERT转TRT反而变慢大概率不是姿势问题,是算子融合没吃透。你试试把动态shape彻底锁死,包括batch和seq_len都固定,再用trtexec转一遍engine,很多时候动态shape的开销比想象中大得多。另外onnxruntime-gpu走的是它自己的CUDA EP,跟TensorRT完全两码事,你如果真想用TRT加速得直接走tensorrt EP或者导出.engine文件。还有个小细节,检查下attention mask是不是被当成普通输入参与计算了,这玩意儿经常导致不必要的拷贝。我上次是改成静态shape后从15ms降到6ms,但动态shape怎么调都回不去。
110M的BERT转TRT确实容易踩坑,动态shape的padding开销有时候比省掉的还多。建议试试固定到训练时的真实分布长度,或者开half精度,通常能有明显改观。另外onnxruntime的CUDA EP对transformer支持一般,不如直接用TensorRT的Python API搭,效果差异挺大的。你profiler里能看下具体算子耗时吗?我怀疑是softmax或者layer norm被拆成小kernel了。
我之前也踩过这个坑,110M的BERT转TRT反而变慢太正常了,尤其动态shape的时候,TensorRT会为每个可能的seq_len做kernel autotuning,这个开销直接摊到单次推理里了。你固定到128还是慢,我怀疑是MultiHeadAttention里的算子被拆得太碎,TRT对这类小算子组合的调度效率反而不如PyTorch的原生CUDA kernel。另外onnxruntime-gpu不一定真的调用了TensorRT,你得确认下是不是走的CUDA execution provider而不是TensorRT execution provider,这俩差别很大。建议你先试试把onnx转成TensorRT的engine文件再推理,别直接用onnxruntime的TRT EP,这样能绕过很多图优化的坑。还有个思路是试试把模型量化到FP16,BERT在TRT下精度损失不大,但速度能上来不少。你profiler显示在CUDA上不代表没有算子fallback,之前我遇到过LayerNorm被拆成多个小kernel,每个都在GPU上但开销翻倍。最后想问下,你测的12ms是warmup之后的结果吗?如果没做warmup,那个base line本身就虚高,对比起来会有误导。
遇到过类似的,BERT转ONNX用TRT确实容易踩坑,尤其是动态shape时显存分配和kernel选择会拖后腿。你试过把onnxruntime的execution_mode设成ORT_SEQUENTIAL没?有时候并行模式在小模型上反而开销大。另外110M参数转TRT不一定划算,可以对比下onnxruntime直接跑fp16,说不定比你现在快。我上次是改用TensorRT的显式batch+固定seq_len才把延迟压下来,动态shape在TRT上对Transformer真不友好。
之前我也踩过类似的坑,BERT类模型转TRT经常遇到小算子融合不充分的问题,110M参数单条12ms已经很快了,ONNX那层反而可能引入额外拷贝开销。动态shape在TRT里会触发多档优化,但preview阶段没选对精度模式的话收益确实有限。建议你先试试把整个encoder换成TRT的fused MHA,或者直接用FasterTransformer那个路径,另外检查下是不是输入/输出在CPU和GPU之间来回搬运了。你profiler里能看到kernel占用率吗,我怀疑是memory bound而不是compute bound。
试试固定seq_len+全静态shape,动态shape对TRT优化影响真挺大的,我这bert转完能快两倍。
ONNX Runtime的CUDA EP对BERT这类模型其实优化一般,很多算子还是走老版本实现,反而没PyTorch的融合做得好。你试试把动态shape彻底锁死,包括batch size也固定,或者直接用TensorRT的Python API从PyTorch导出engine,绕过ONNX中间层。另外检查一下是不是输入做了padding导致计算量虚高,110M模型12ms已经很极限了,ONNX转完不一定有提升空间。
我之前也踩过类似的坑,110M的BERT转TRT反而变慢大概率不是姿势问题,是TensorRT对Transformer里动态shape的算子融合本来就保守,尤其attention那块容易生成低效kernel。你可以试试把seq_len固定后顺便关掉动态shape的维度优化,或者干脆用onnxruntime的transformers优化工具先剪一遍图,再把优化后的onnx转TRT,我这边这样搞能压到8ms左右。另外确认下是不是用了fp32,转成fp16有时候能直接救回来。
我之前也踩过类似的坑,110M的模型转TRT不升反降大概率是动态shape惹的祸,试试把min/max/opt三个维度都设成同一个固定值,或者干脆用静态shape,性能会立刻不一样。另外BERT这类模型里LayerNorm和Gelu的算子融合在ONNX Runtime里做得并不好,建议用onnxsim精简一下图结构,再配合TRT的FP16精度试试。还有个思路是直接用TensorRT的官方bert demo那种流程,自己搭plugin,但工程成本就上去了。你profiler里显示在CUDA上,但有没有留意过CPU和GPU之间的数据拷贝次数?有时候小模型反而被拷贝开销拖累了。
说实话你这个情况我太熟了,之前我转一个ALBERT的时候也这样,TensorRT对动态shape的优化特别保守,尤其是attention那块,它经常会把QKV拆成几个小算子,反而增加了kernel launch的开销。你试试把onnxruntime的provider换成CUDA而不是TensorRT,有时候ORT自己的融合逻辑反而比TRT快,特别在动态shape下。另外你确认过是不是真的跑在TensorRT上了吗?有时候你装了trt的provider但actual provider还是CUDA,因为某些算子不支持就会自动回退,profiler看着在CUDA上不代表TRT生效了。还有个小技巧,把DynamicAxis的优化关掉,只固定batch为1,seq_len留动态,很多情况下能逼TRT走它最擅长的静态shape优化路径。要是还不行,建议直接把attention部分用TRT的plugin重写,或者干脆用opset 14以上版本,新算子映射会好很多。我最后是换成C++ API才把延迟压到8ms的,Python的pythonic overhead真的不能忽略。你那个12ms是纯gpu时间还是包含数据搬运?如果包含的话,说不定瓶颈根本不在推理,而在dataloader的collate逻辑上,多测几次把preprocessing时间单独算出来看看。
我之前也踩过类似的坑,BERT类模型转ONNX后变慢大概率是算子融合没生效,特别是MultiHeadAttention里的reshape和transpose,TensorRT对动态shape的优化确实不如静态shape。你可以试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,或者干脆用tensorrt直接加载pt权重,我这边同参数模型TRT能压到7ms左右。另外确认下是不是输入tensor的memory格式问题,有时候NCHW和NHWC切换也会白吃性能。
动态shape对TRT的优化影响挺大的,试试把min/opt/max都固定成一样,能快不少。
ONNX Runtime的Transformer fusion没完全生效吧,检查下有没有走FlashAttention或者手写个custom op。
之前跑过类似的模型,110M的BERT转TRT确实容易遇到这个问题。动态shape在TensorRT里会触发很多额外优化开销,尤其是attention那块,建议先用onnxruntime的CUDA EP跑一下固定shape对比,如果还是慢那大概率是op融合没生效。另外检查下是不是有LayerNorm或Gelu被拆成多个小算子,这些在TRT里特别容易卡性能瓶颈,手动合并一下可能比开自动优化管用。还有个思路是直接用TensorRT的官方BERT demo配置,他们那套对seq_len做了特殊处理,直接套用能省不少调试时间。
我之前也踩过类似的坑,BERT转ONNX用TRT反而慢真不稀奇。你试过把dynamic axis关掉,用固定shape加trt的min/max/opt profile吗?有时动态shape在TRT里会走通用kernel,反而没静态优化得狠。另一个思路是检查一下是否用了fp16,BERT的gelu和layernorm在TRT里有时会触发fallback,虽然profiler显示在CUDA但可能混着精度模式跑。建议直接比较一下onnxruntime和trt的算子执行时间,看看具体卡在哪个节点上。
110M的BERT转TRT反而变慢,我怀疑是动态shape惹的祸,TensorRT对动态维度要重新做engine优化,每次推理都有额外开销,你可以试试把batch和seq_len都固定死,哪怕多导出几个版本按需加载,速度应该能上来。另外onnxruntime-gpu走的其实是CUDA EP,不是TensorRT,想用TRT得显式指定trt execution provider,这俩差距挺大的。我之前跑蒸馏版BERT也遇到过类似情况,最后是砍掉一些不常用的OP,比如把GELU换成ReLU才勉强持平PyTorch,Transformer结构里LayerNorm和Attention的矩阵运算在TRT上优化得并不好。你profiler里看着在CUDA,但说不定有隐式拷贝或者算子融合没生效,建议开--dump-profile看看具体kernel耗时分布。
110M的模型单条12ms其实已经挺快了,ONNX这边慢大概率是dynamic shape没吃透,试试把seq_len固定成训练时的值,另外确认下attention mask是不是也跟着转过去了,缺失的话ORT会走fallback路径。我之前也是BERT转TRT,最后发现是LayerNorm精度设置问题,你检查下TRT的FP16开关,有时候混精度反而比FP32更慢。另外onnxruntime-gpu和TensorRT是两回事,你如果真要用TRT得走trtexec转engine,光靠ORT后端提升有限。
试试把dynamic axes全关掉,BERT这种模型固定shape收益最大,我上次直接快了一倍多。