最近在部署一个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的模型转ONNX后变慢太正常了,别急着怀疑姿势。你固定seq_len到128还是慢,大概率是Embedding层和Attention里的Gather、Unsqueeze这些算子被拆得太碎,TRT对动态shape的kernel融合效率远不如静态图,尤其是BERT这种结构,很多小算子根本走不了CUDA的TensorCore。我建议你先用onnxruntime的CAPI直接跑一遍,对比下onnxruntime-gpu和TRT的backend差异,另外试试把dynamic_axes全去掉,用固定shape导出,如果速度能压到8ms以内,那基本就是动态shape的调度开销在作怪。还有个思路,你试试把LayerNorm和Softmax换成TRT自带的高效实现,或者用onnx-simplifier把图精简一遍,有时候冗余的Identity节点就能拖慢30%。另外你确认过onnxruntime的intra_op线程数没?默认可能会抢占CPU资源,导致GPU等待。我自己的经验是,这种小模型TRT收益本来就不大,如果延迟敏感,不如直接上TensorRT的PythonAPI手动搭engine,别走ONNX中转。你profiler显示在CUDA上,但有没有看具体每个op的耗时?我怀疑是KV Cache相关的动态shape算子触发了重新编译。
110M的模型12ms已经不错了,ONNX这层皮不是万能的,重点看算子映射和TRT的优化空间。
动态shape确实容易触发重编译,固定长度还慢就得查下是不是有算子没吃到TRT的kernel。
之前调过一个类似的小模型,动态shape在TensorRT里确实容易触发隐式重编译,建议先用固定shape试试,同时把opset拉到17以上。另外onnxruntime-gpu和TensorRT是两回事,你直接跑onnxruntime可能是走的CUDA EP而非TensorRT EP,这俩性能差距能到好几倍。还有个小坑,BERT的attention mask如果没处理好,会被拆成一堆小算子,反而拖慢速度,可以考虑用trt的插件或者干脆在导出时把mask合并进权重里。最后问下,你profiler里看的是单次推理时间还是包含了内存拷贝?有时候H2D/D2H的耗时占比很高但容易被忽略。
大概率是动态shape把TensorRT的kernel选择搞崩了,试试固定到64或者32看下差距。
我上次也这样,后来发现是op融合没生效,换个onnx-simplifier试试。
我之前也踩过类似的坑,不过是在图像模型上。110M的BERT转TRT变慢,大概率不是框架问题,而是你的动态shape把优化空间卡死了。TensorRT对Transformer的attention结构其实有专门优化,但前提是shape要完全静态,哪怕你固定了seq_len,如果batch维度还留了动态,它内部的kernel选择策略就会变得特别保守,反而比不上PyTorch那种即时编译的灵活性。
另外有个细节你可能忽略了,onnxruntime-gpu默认走的是CUDAExecutionProvider,但它内部对Transformer的算子融合做得并不好,很多小算子拆得太碎,每次kernel launch的开销在短序列上占比极高。建议你直接绕开ORT,用TensorRT的Python API重新搭一个engine,或者试下torch-tensorrt,它能把PyTorch的graph整体编译进去,动态shape支持得更自然。
还有个歪招,你可以把模型量化到FP16再对比一下,BERT这类模型半精度下通常能快两倍,但要注意LayerNorm的精度衰减。如果还不行,不妨检查一下输入数据的memory layout,ONNX默认是NCHW,但PyTorch内部可能已经用了NHWC的变体,有时候就差这么一点。
最后想问下,你profiler里看到的kernel时间占比是多少?如果大部分时间都花在小的elementwise操作上,那基本就是算子融合没生效,这时候固定所有维度到最极端(比如batch=1, seq=128)再试一次,如果速度还是没变化,那就真该考虑换个方案了。
碰到过类似的坑,但你这个情况大概率不是TensorRT不会处理Transformer,而是ONNX导出时把动态shape的某些算子搞成了低效实现。我之前有个小模型也是,转完onnxruntime-gpu反而比PyTorch慢,后来发现是attention里的reshape和transpose被拆成多个小算子,导致kernel launch开销剧增,12ms变18ms很正常。你试试用onnx-simplifier过一遍图,或者直接导出时把dynamic_axes去掉,固定seq_len到128但把batch留动态,有时候反而更快。另外确认下onnxruntime的版本,老版本对某些op的优化很差,换最新的1.17或1.18可能就有改善。还有个思路是别用onnxruntime,直接用TensorRT的parser从PyTorch导出的ONNX转engine,虽然麻烦但实测对小BERT模型能压到8ms左右。你profiler显示在CUDA上但没具体看kernel时间占比吧?我怀疑大部分时间花在了数据拷贝和小的elementwise操作上。最后问一下,你输入是fp32还是fp16?如果没开混合精度,这50%的差距可能纯粹是精度没对齐导致的。
我之前也踩过类似的坑,110M的BERT转TRT反而更慢大概率不是框架问题,是动态shape导致的kernel选择太保守。你可以试试把seq_len固定成你实际推理最常用的值,然后用trtexec导出engine,别用onnxruntime直接加载,它优化Transformer的能力远不如纯TRT。另外检查下是不是attention的mask被拆成多个小算子,合并成原生MultiHeadAttention节点能快不少。还有个偏方,把fp16开了,很多时候精度损失可接受但速度能翻倍。你profiler看到在CUDA上不代表没走efficient kernel,建议直接对比一下每个节点的耗时,重点看Softmax和MatMul。
动态shape加TRT对BERT优化确实容易翻车,建议先试试固定batch和seq_len,顺便看下是不是attention_mask拖了后腿。
我之前也踩过类似的坑,110M的BERT转TRT后反而慢。后来发现问题出在动态shape上,TensorRT对动态尺寸的优化远不如静态,而且注意力矩阵的reshape和softmax很容易触发低效的kernel。你可以试试把动态维度拆成多个静态batch或固定长度,或者换用FasterTransformer那套融合算子,别指望纯ONNX导出能自动优化。另外确认下是不是用了fp32,fp16有时候能快一倍还不掉点。
试试把动态shape全关死,seq_len固定成训练时的值,TRT对BERT的动态轴优化很拉胯。
我之前也遇到过,onnxruntime的CUDA EP反而比TensorRT快,换个EP试试。
我之前也踩过类似的坑,110M参数转TRT有时候反而更慢,大概率不是框架问题,而是动态shape惹的祸。你固定seq_len到128试过还是慢的话,建议看看是不是attention里的softmax或者某些op被拆成了多个小kernel,导致kernel launch开销占比太大。另一个思路是试试用onnxruntime直接跑静态shape,排除TensorRT的图优化瓶颈,我这边之前把opset版本从13升到17之后速度才正常。还有个小细节,你profiler看到CUDA不代表没走CPU fallback,有些融合算子会半CPU半GPU,建议抓一下timeline看具体每层耗时。
大概率是动态shape导致TensorRT没吃到最优kernel,试试固定batch和seq_len再开trt的fp16,应该能反超。
小模型瓶颈在启动开销和内存拷贝,12ms到18ms可能是数据搬运占了大头,你看看CPU和GPU之间的传输时间。
你试试把dynamic axes关掉,用固定shape导出一次对比下,我之前遇到过类似情况,动态shape在某些版本下会触发额外的transpose和copy操作,反而拖慢速度。另外onnxruntime的CUDA EP对Transformer的支持其实一般,不如直接用TensorRT的python API走一遍,或者试试onnxruntime的TensorRT EP,有时候比直接CUDA EP快不少。还有个思路是检查下是不是模型里有LayerNorm之类的算子被拆成了多个小算子,导致kernel launch开销变大,这个在profile里能看出来。
110M的BERT转TRT变慢大概率是动态shape惹的祸,试试静态shape加int8量化,效果立竿见影。
你这情况像是算子融合没吃透,建议直接对比下onnxruntime和TRT的逐层耗时,问题就暴露了。
动态shape加TRT本来就容易踩坑,试试固定到32的倍数或者用onnx-tensorrt直接导出看看。
我当时转T5也这样,后来发现是attention mask那路算子拖后腿,你查下是不是这块。
我之前也踩过类似坑,110M的BERT转TRT反而变慢大概率不是姿势问题,而是TensorRT对动态shape的Transformer结构优化本来就一般,尤其是attention里的reshape和transpose容易触发低效实现。你试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,同时关掉cudnn_conv_algo_search,有时候默认的并发策略在短序列上反而拖慢。另外确认下是不是真的走了TRT EP,有时候provider没配对会静默用CPU kernel,哪怕profiler显示CUDA也可能是显存拷贝的假象。实在不行可以对比下固定seq_len且batch=1的静态onnx,如果还慢就基本是框架调度开销大于模型本身了。
动态shape对TRT不友好,试试固定到32或64的倍数,另外检查下attention mask是不是被强制CPU了。
110M的BERT转TRT确实容易踩坑,我之前试过类似模型,动态shape如果没配好优化profile,TensorRT会频繁重新选kernel,反而比静态图慢。你试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,或者干脆用tensorrt直接转engine,别套ORT的壳。
另外检查下attention mask是不是被当成输入了,有些算子会导致graph分裂,CUDA上跑但实际在走离散kernel。我之前是固定到64的seq_len才勉强提速,动态shape对Transformer来说收益真不大。
要不你试试把LayerNorm换成融合版本?有时候这些小算子才是瓶颈。
试过把dynamic axes全关掉用静态shape吗?BERT转TRT经常是长度自适应拖垮性能。
onnxruntime对transformer支持确实一般,试试直接转TensorRT引擎,跳过ONNX这层可能更快。
我之前也踩过类似的坑,ONNX导出后不一定就快,尤其是BERT这类带attention的模型,TensorRT对动态shape的支持其实挺吃配置的。你可以试试把onnxruntime的session选项里加个cudnn_conv_algo_search,或者直接对比一下固定batch和seq_len的trt engine,有时候反而是静态shape能逼出优化空间。另外确认下onnxruntime是不是真的走了TensorRT EP,有时候默认的CUDA EP也会显示在GPU上,但算子融合差很多。我这边后来是直接用torch_tensorrt才把延迟压下去的,onnx中间层反而有点折腾。