📺 B站博主个人介绍

📘 博主书籍-京东购买链接*:Yocto项目实战教程

📘 加博主微信,进技术交流群jerrydev


一文吃透 TensorRT 实战:从 ONNX 到 Engine,再到高性能推理落地

这篇文章我想用“实战 + 原理 + 工程经验”的方式,把 TensorRT 讲清楚。很多人第一次接触 TensorRT,会把它理解成“ONNX 转换工具”;但真正上手后会发现,它更像一个面向 NVIDIA GPU 的推理编译器 + 运行时系统。如果只记接口,很快会乱;如果把它放回“构建阶段”和“运行阶段”去理解,很多 API、性能现象和报错就都能串起来了。TensorRT 官方也明确把它定义为一个用于优化已训练模型、并执行高性能推理的 SDK,核心由 inference optimizer 和 runtime 两部分组成。(NVIDIA Docs)


一、为什么我要单独写 TensorRT

我在真正开始用 TensorRT 之前,最困惑的不是“某个 API 怎么写”,而是这几个问题:

  1. TensorRT 到底是做模型转换,还是做模型执行?
  2. ONNX、Engine、Runtime、ExecutionContext 之间到底是什么关系?
  3. FP32、FP16、INT8 到底差在哪,什么时候值得开?
  4. 动态 shape、plugin、workspace、timing cache 这些工程概念到底该怎么理解?

这些问题如果不先理顺,后面就会出现一种非常常见的现象:代码能抄出来,但不知道为什么这么写,也不知道什么时候该改。 而 TensorRT 的官方文档其实已经把主线讲得很清楚:它分成 build phaseruntime phase 两部分,前者负责把模型优化成 plan/engine,后者负责把 engine 真的跑起来。(NVIDIA Docs)


二、TensorRT 到底是什么

先给一句我认为最准确的定义:

TensorRT 不是训练框架,而是 NVIDIA 面向推理部署的优化器和运行时。

也就是说,它不是拿来训练模型的,而是把已经训练好的模型进一步优化,让它在 NVIDIA GPU 上跑得更快、延迟更低、吞吐更高。官方 quick start 也是这么定义的:TensorRT 在训练之后介入,把模型优化成适合目标硬件执行的形式,再由 runtime 执行推理。(NVIDIA Docs)

我现在更愿意把 TensorRT 理解成三层:

层次 作用 我怎么理解
模型输入层 ONNX / 手工 Network Definition “我要推理什么模型”
编译优化层 Builder / Config / Parser “怎么把它编译成适合当前 GPU 的 Engine”
运行执行层 Runtime / Engine / Context “怎么把这个 Engine 真正跑起来”

上面这张表,是我对官方能力模型的工程化总结:TensorRT 接受模型定义,在构建阶段做优化并生成序列化的 plan;运行阶段反序列化 plan,创建 execution context 执行推理。(NVIDIA Docs)

在这里插入图片描述


三、先建立最重要的心智模型:Build Phase 和 Runtime Phase

这是我认为学习 TensorRT 最关键的一步。

1. Build Phase:离线编译模型

构建阶段通常是离线做的。你把 ONNX 或者 Network Definition 交给 TensorRT,TensorRT 会为目标 GPU 选择合适的 tactic、kernel、布局和执行方案,然后生成一个序列化产物,也就是 plan / engine 文件。官方明确说明 builder 会在构建过程中为层实现做计时,因此构建阶段本质上很像一次“面向硬件的推理编译”。(NVIDIA Docs)

2. Runtime Phase:在线执行推理

运行阶段则很纯粹:把 plan 反序列化成 engine,从 engine 创建 execution context,把输入输出地址绑定进去,然后执行推理。官方 Python 文档对应的是 execute_async_v3(),C++ 文档对应的是 enqueueV3()。(NVIDIA Docs)

3. 一张流程图看懂主链路

[PyTorch / TensorFlow / 其他训练框架]
                |
                v
         [导出 ONNX 模型]
                |
                v
   [TensorRT Build Phase / Builder]
      - Parser 解析模型
      - Config 设置策略
      - tactic/kernel 选择
      - 生成 serialized engine(plan)
                |
                v
      [保存 .plan / .engine 文件]
                |
                v
    [TensorRT Runtime Phase / Runner]
      - Runtime 反序列化 engine
      - Engine 创建 ExecutionContext
      - 绑定输入输出 GPU 地址
      - execute_async_v3 / enqueueV3
                |
                v
             [推理输出]

这条链路对应的就是官方 programming model:第一阶段优化,第二阶段执行。(NVIDIA Docs)

在这里插入图片描述


四、ONNX、Engine、输入数据,到底是什么关系

这也是实战中最容易混淆的一件事。

1. ONNX 解决的是“模型是什么”

ONNX 文件里保存的是模型图、算子、权重、输入输出规格。TensorRT 的 ONNX Parser 会把这些内容导入到 TensorRT 的 network definition 里。(NVIDIA Docs)

2. Engine 解决的是“这个模型如何在当前 GPU 上高效执行”

Builder 会读取 network definition 和 config,然后生成一个优化后的 engine。这个 engine 可以立刻反序列化,也可以存盘以后部署。默认情况下,TensorRT engine 同时依赖 TensorRT 版本和构建它的 GPU/兼容策略;官方 support matrix 也明确说明,序列化 engine 默认并不是跨平台、跨 GPU、跨版本完全可移植的。(NVIDIA Docs)

3. 输入数据解决的是“这次我要算什么”

这里很多人会误会,以为 ONNX 转 Engine 的时候必须有真实图片。其实正常 FP32 / FP16 构建并不需要真实输入样本,输入数据是在运行阶段才真正参与推理的。例外是 INT8 校准这类量化流程,才可能需要有代表性的真实数据分布。官方量化文档明确提到,显式量化是推荐方向,而隐式 INT8/校准是另一条历史路径。(NVIDIA Docs)

我自己的记忆方式是:

  • ONNX:描述模型
  • Engine:描述如何高效执行模型
  • 输入张量 x:描述这次实际要计算的内容

五、Builder 这套 API,到底固定到什么程度

如果只看 ONNX -> Engine 这一段,主骨架基本是固定的。Python 和 C++ 的 API 名称虽然不同,但职责是一一对应的。

1. Python / C++ 构建 API 对照表

阶段 Python API C++ API 作用
日志器 trt.Logger(...) 自定义 ILogger 打印解析和构建日志
创建 Builder trt.Builder(logger) createInferBuilder(logger) 创建构建器
创建 Network builder.create_network(0) builder->createNetworkV2(0U) 创建 TensorRT 网络定义
解析 ONNX trt.OnnxParser(...).parse_from_file(...) createParser(...)->parseFromFile(...) 把 ONNX 导入 network
创建 Config builder.create_builder_config() builder->createBuilderConfig() 设置构建参数
设置 workspace config.set_memory_pool_limit(...) config->setMemoryPoolLimit(...) 限制构建时内存池
构建 plan build_serialized_network(...) buildSerializedNetwork(...) 生成序列化 engine
反序列化 engine runtime.deserialize_cuda_engine(...) runtime->deserializeCudaEngine(...) 生成可运行 engine

表中的主干 API 都来自官方 Python/C++ 文档。值得注意的是,TensorRT 10 中网络已经总是 explicit batch,因此老教程里常见的 EXPLICIT_BATCH 在 10.x 中已被标记为 deprecated/ignored。(NVIDIA Docs)

2. 真正会变化的,是配置而不是骨架

也就是说,代码结构通常不怎么变,变化的是:

  • 是否使用动态 shape profile
  • 是否启用 FP16 / INT8 或强类型网络
  • 是否需要 plugin
  • workspace 和 tactic 相关设置
  • 是否需要版本兼容 / 硬件兼容

这也是为什么很多工程代码看起来像“模板”:主流程几乎一样,区别都在 config 和附加能力上。官方 capabilities、advanced、dynamic shapes、plugins 文档都在围绕这些可变点展开。(NVIDIA Docs)


六、Runner 这套 API,才是真正的推理执行链路

很多人学 TensorRT 只盯着 builder,结果到了真正执行阶段就乱了。其实 runtime 部分的主线更清晰。

1. Runner 的三大核心对象

对象 作用 我的理解
Runtime 反序列化 plan “加载器”
ICudaEngine 优化好的模型 “编译产物”
ExecutionContext 执行一次推理 “真正的 runner”

官方明确说明 execution context 是调用 inference 的主接口,一个 engine 可以创建多个 context。(NVIDIA Docs)

2. Runner 的核心调用链

阶段 Python API C++ API 作用
加载 engine deserialize_cuda_engine() deserializeCudaEngine() 从 plan 反序列化
创建 context create_execution_context() createExecutionContext() 创建执行环境
设置动态 shape set_input_shape() setInputShape() 指定本次输入 shape
绑定地址 set_tensor_address() setTensorAddress() 绑定输入输出显存
启动推理 execute_async_v3() enqueueV3() 提交 CUDA stream 上执行

这张表总结自官方 Python/C++ runtime 文档和 dynamic shape 文档。(NVIDIA Docs)

3. 我对 runner 的一句话理解

Builder 负责把模型“编译好”,Runner 负责把编译结果“跑起来”。

这句话虽然简单,但能帮我把很多 API 自动归类:凡是 builder、parser、config 一类的,大概率属于构建期;凡是 runtime、engine、context、tensor address 一类的,大概率属于运行期。


七、FP32、FP16、INT8:到底该怎么理解

这一块如果讲得不清楚,很容易要么“盲目开 INT8”,要么“完全不敢碰量化”。

1. 先说结论

  • FP32:最稳,通常也是原始模型默认精度。
  • FP16:通常是 TensorRT 实战里最常见、最稳妥的第一步。
  • INT8:常常能进一步提高吞吐、降低延迟和带宽占用,但有一定精度风险,必须做验证。

TensorRT 的高级主题和量化文档都强调,降低精度是为了提升性能,但会引入准确率风险;不同层也不一定都会真的用低精度执行。(NVIDIA Docs)

2. 这三种精度在 API 上有什么不同

如果是传统写法,差异主要体现在 BuilderConfig 上;但从 TensorRT 10.12 开始,weak typing 已经 deprecated,官方推荐 strong typing 和 explicit quantization。在 strongly typed network 下,很多旧式的 builder precision flags 本身就是不允许的。(NVIDIA Docs)

精度模式 典型作用 主要变化位置 需要注意什么
FP32 基线精度 基本无额外精度配置 稳,但通常性能不是最优
FP16 常见加速选项 传统写法在 BuilderConfig 上启用 现代版本更推荐走强类型路径
INT8 更激进的优化 量化模型 / QDQ / 或历史校准链路 需要做精度验证

这张表是基于官方 advanced、quantized types、strong typing 文档的工程总结。(NVIDIA Docs)

3. INT8 到底值不值得开

我的建议很明确:

  • 如果你追求极致吞吐、低延迟、低显存和带宽成本,INT8 值得测
  • 如果你对精度特别敏感,先把 FP16 跑通,再决定要不要往 INT8 走。
  • 如果你的模型已经有 Q/DQ,或者做过 QAT,那么 INT8 更值得认真做。

原因也很直接:官方明确建议未来优先采用显式量化,并指出隐式量化已经弃用;同时低精度通常能带来更好的推理性能,但不是没有精度代价。(NVIDIA Docs)


八、动态 Shape:为什么它很重要,但又很容易踩坑

动态 shape 的本质,就是把某些维度延迟到运行时再指定。官方定义里,这些运行时维度通常在构建时用 -1 占位,然后通过 optimization profile 告诉 builder 允许哪些 shape 范围,运行阶段再给 execution context 设置这次实际 shape。(NVIDIA Docs)

1. 动态 shape 的工作流

[ONNX 输入维度含 -1]
        |
        v
[Build 阶段创建 Optimization Profile]
  - min shape
  - opt shape
  - max shape
        |
        v
[生成支持该范围的 Engine]
        |
        v
[Runtime 阶段]
  - 选择合适 profile
  - context.set_input_shape(...)
  - 查询输出 shape
  - 执行推理

这就是官方 dynamic shapes 文档的标准流程。(NVIDIA Docs)

2. 我在工程上怎么理解 profile

min / opt / max 不是随便填的。

  • min:你允许的最小输入
  • max:你允许的最大输入
  • opt:你最常见、最想优化的输入

很多人把 profile 理解成“随便给个范围就行”,结果构建能过,但性能不理想。实际上 builder 会围绕 profile 和 tactic 选择做优化,因此 opt 的设置会直接影响常用场景性能。这个结论是对官方 build/动态 shape 机制的工程化理解。(NVIDIA Docs)


九、一个真正能落地的 Python 实战模板

这一节我给一个最小但清晰的思路:ONNX -> TensorRT Engine -> 推理执行

1. 构建阶段:ONNX 转 Engine

import tensorrt as trt

TRT_LOGGER = trt.Logger(trt.Logger.WARNING)

def build_engine_from_onnx(onnx_path: str):
    builder = trt.Builder(TRT_LOGGER)
    network = builder.create_network(0)  # TensorRT 10.x 中显式 batch 已默认化
    parser = trt.OnnxParser(network, TRT_LOGGER)

    ok = parser.parse_from_file(onnx_path)
    if not ok:
        for i in range(parser.num_errors):
            print(parser.get_error(i))
        raise RuntimeError("Failed to parse ONNX")

    config = builder.create_builder_config()
    config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)  # 1GB

    serialized_engine = builder.build_serialized_network(network, config)
    if serialized_engine is None:
        raise RuntimeError("Failed to build engine")

    runtime = trt.Runtime(TRT_LOGGER)
    engine = runtime.deserialize_cuda_engine(serialized_engine)
    return engine

这段代码对应的就是官方 Python 构建主链路:Builder -> Network -> Parser -> BuilderConfig -> build_serialized_network -> Runtime.deserialize_cuda_engine。其中 workspace 内存池会影响构建时可用的 tactic 空间;官方也说明,如果 workspace 限制过小,会间接限制可选实现。(NVIDIA Docs)

2. 运行阶段:Engine 执行推理

def run_inference(engine, input_name, output_name, d_input, d_output, stream_ptr):
    context = engine.create_execution_context()

    # 动态 shape 模型时,先设置 shape
    # context.set_input_shape(input_name, (1, 3, 224, 224))

    context.set_tensor_address(input_name, int(d_input))
    context.set_tensor_address(output_name, int(d_output))

    context.execute_async_v3(stream_ptr)

这就是官方 Python runtime 的核心执行形态。需要注意的是,这里绑定的是 GPU 显存地址,不是 NumPy 数组本身;真正的数据搬运通常要靠 CUDA Python、PyCUDA、PyTorch、CuPy 等去管理。官方 Python 文档也明确提到,GPU 内存分配可以来自多种 Python CUDA 生态工具。(NVIDIA Docs)

3. 这个例子最重要的学习点

我觉得这段代码里最值得学的不是“怎么复制”,而是下面两句话:

  • build_engine_from_onnx() 只负责模型构建
  • run_inference() 才负责模型执行

这正好对应 TensorRT 的 build/runtime 两阶段设计。只要这个边界一清楚,很多问题都不容易混淆。(NVIDIA Docs)


十、trtexec:我最推荐先会用的命令行工具

如果是工程落地,我很建议先把 trtexec 学会。因为它能帮我们快速做三件事:

  1. 验证 ONNX 是否能被 TensorRT 正常解析
  2. 快速生成 engine
  3. 做基础 benchmark 和 profiling

官方 best practices 和 command-line 文档都把 trtexec 作为重要工具来介绍。(NVIDIA Docs)

1. 最基础的构建命令

trtexec --onnx=model.onnx --saveEngine=model.plan

这条命令本质就是把 ONNX 编译成 TensorRT plan。(NVIDIA Docs)

2. 动态 shape 的常见写法

trtexec \
  --onnx=model.onnx \
  --minShapes=input:1x3x224x224 \
  --optShapes=input:4x3x224x224 \
  --maxShapes=input:8x3x224x224 \
  --saveEngine=model.plan

官方命令行文档明确支持 --minShapes / --optShapes / --maxShapes 这组参数来描述输入 shape 范围。(NVIDIA Docs)

3. benchmark 的意义

我自己的建议是:先用 trtexec 跑出一个可信基线,再上业务代码。
因为如果 trtexec 都跑不顺,问题大概率还在模型、算子支持、shape 或环境层面;如果 trtexec 很快,业务代码却很慢,那就多半是你自己的数据搬运、预处理、后处理、同步方式、stream 使用或 batch 策略有问题。官方 best practices 也是围绕 benchmark、profile、分析瓶颈来展开。(NVIDIA Docs)


十一、性能优化时,我会优先看哪些点

1. 不要把构建时间和运行时间混为一谈

Builder 在构建时要做 tactic 计时,所以 build 慢并不代表 runtime 慢;反过来,构建得很快也不代表 runtime 就一定快。官方 how TensorRT works 文档明确解释了构建阶段会进行实现计时,并可能消耗相当多的临时显存。(NVIDIA Docs)

2. workspace 不是越小越好

workspace 太小会限制某些高性能 tactic 的选择,导致最终 engine 性能下降;但也不是盲目越大越好,尤其在资源受限环境里需要折中。官方文档对 workspace 的作用和默认行为有明确说明。(NVIDIA Docs)

3. 先做 FP16,再考虑 INT8

这是我最推荐的实践顺序。FP16 往往能带来不错的性能提升,同时精度风险通常比 INT8 更容易控制;INT8 则更适合在有明确收益目标、且能做严谨验证的时候再上。这个顺序并不是官方硬性要求,但与官方对量化收益/风险和平衡方式的描述是一致的。(NVIDIA Docs)

4. 动态 shape 的 profile 要贴近真实业务

如果 opt shape 和线上常用输入偏差很大,engine 很可能“理论支持你的输入”,但并没有为你的主流场景做到最佳优化。这个点官方虽然不会用我的话表述,但从 optimization profile 的设计目的就能推导出来。(NVIDIA Docs)

5. 真正复杂的模型,要准备 plugin 方案

TensorRT 支持非常多的层,但并不是所有模型结构都能无缝原生落地。官方专门提供了 custom layers / plugins 机制,当前推荐使用 IPluginV3 和对应的 plugin creator 体系。(NVIDIA Docs)


十二、几个非常容易踩的坑

坑 1:把 TensorRT 当成“万能模型转换器”

TensorRT 的强项不是“把格式改掉”,而是“在 NVIDIA GPU 上做高性能推理”。如果只看格式转换,就低估了它真正的价值。官方 quick start 和 capabilities 都强调它是 optimizer + runtime,而不只是 parser。(NVIDIA Docs)

坑 2:以为 Engine 一次构建,到处都能跑

默认不是。官方 support matrix 明确指出:序列化 engine 默认不跨平台,也不天然跨 GPU 架构;版本兼容和硬件兼容需要额外配置,而且兼容模式可能带来一定性能损失。(NVIDIA Docs)

坑 3:只看能不能跑,不看精度

这在 INT8 上尤其危险。低精度推理最怕“速度很好,但结果已经不可用”。官方量化和 accuracy/performance 资料都强调,精度验证是必要步骤。(NVIDIA Docs)

坑 4:还在照搬很老的教程

TensorRT 10.x 已经对很多旧概念做了现代化调整,比如 explicit batch 总是开启、weak typing 在 10.12 被弃用、官方推荐 explicit quantization、plugin 方向转向 IPluginV3。如果还照抄很老的 7.x/8.x 文章,后面碰到 API 过时是必然的。(NVIDIA Docs)


十三、我会怎么规划一条 TensorRT 学习路线

如果让我重新学一次 TensorRT,我会按下面这个顺序来:

阶段 学什么 为什么这么排
第 1 步 Quick Start + trtexec 先跑通全流程,建立直觉
第 2 步 Build / Runtime 主链路 搞清 Builder 和 Runner 分工
第 3 步 动态 shape 实际部署几乎绕不开
第 4 步 FP16 / INT8 / 显式量化 决定性能上限
第 5 步 Profiling / Best Practices 做真实优化
第 6 步 Plugin / 兼容性 / 排障 面对复杂模型和生产问题

这个顺序和官方文档组织方式是吻合的:quick start、capabilities、dynamic shapes、quantized types、best practices、custom layers、troubleshooting 本来就是一条由浅入深的路线。(NVIDIA Docs)


十四、全文总结:把 TensorRT 归结为一句话

如果我只用一句话总结 TensorRT,我会写成这样:

TensorRT 是一个把训练好的模型,面向 NVIDIA GPU 编译成高性能推理 Engine,并通过 Runtime 高效执行的系统。

再拆开一点,就是下面这 8 个知识点:

  1. TensorRT 只做推理,不做训练。(NVIDIA Docs)
  2. 它分成 build phase 和 runtime phase。(NVIDIA Docs)
  3. ONNX 是模型定义,Engine 是优化后的执行产物。(NVIDIA Docs)
  4. Builder 是“编译器”,ExecutionContext 是“runner”。(NVIDIA Docs)
  5. 动态 shape 需要 profile,运行时还要设置 input shape。(NVIDIA Docs)
  6. FP16 是很常见的第一步优化,INT8 更激进但要做精度验证。(NVIDIA Docs)
  7. 新版本更推荐 strong typing、explicit quantization、IPluginV3。(NVIDIA Docs)
  8. trtexec 是最值得先掌握的工程工具之一。(NVIDIA Docs)

十五、适合放在文末的实战 Checklist

[ ] 我已经明确模型是推理部署,不是训练流程
[ ] 我知道 ONNX、Engine、Runtime、Context 的区别
[ ] 我能独立跑通 ONNX -> TensorRT Engine
[ ] 我知道 Builder 和 Runner 是两套不同职责的 API
[ ] 我知道动态 shape 要配 profile
[ ] 我知道 FP32 / FP16 / INT8 的收益和风险
[ ] 我知道 INT8 不是“必然更好”,必须验证精度
[ ] 我知道 trtexec 可以先做基线 benchmark
[ ] 我知道 engine 默认不天然跨平台、跨 GPU、跨版本
[ ] 我知道复杂模型可能需要 plugin

如果上面这份 checklist 你已经能全部解释清楚,那么 TensorRT 这条主线,基本就算真正入门了。(NVIDIA Docs)


推荐标签: TensorRT 深度学习部署 ONNX CUDA GPU推理优化 模型加速

如果你要继续把这篇文章做成系列,下一篇最自然的主题就是:“TensorRT Python 实战:从 ResNet50 到动态 Shape,再到 FP16/INT8 对比测试”


📺 B站博主个人介绍

📘 博主书籍-京东购买链接*:Yocto项目实战教程

📘 加博主微信,进技术交流群jerrydev


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐