Skip to content

TensorRT 与 GPU 推理

本页速览 NVIDIA 自家的 GPU 推理优化引擎,通过图融合、kernel auto-tuning 与 INT8/FP8 量化,把训练框架的"通用计算"打磨成"GPU 上的极致吞吐"。本文拆解 Builder/Engine/Runtime 三段式流水线、INT8 calibration、Plugin 接口与 torch_tensorrt 集成。

TensorRT 与 GPU 推理

一、概念定义:把"通用图"打磨成"GPU 上的极速图"

TensorRT 是 NVIDIA 自 2015 年起推出的 GPU 推理优化引擎(inference accelerator)。它的输入是 PyTorch / TensorFlow / MXNet 训练出来的"通用计算图",输出是一份绑定特定 GPU 架构、经过层层优化的"Plan 文件"——你可以把它理解为"为这张卡、这个 batch、这个精度编译出来的本地可执行文件"。

PyTorch 模型 (.pt)
      │  tracing / scripting

ONNX / TorchScript 中间表示
      │  TensorRT Builder(图优化 + kernel 自动调优 + 量化)

Plan 文件 (.plan/.engine)  ← 绑定 GPU 架构 + 精度 + shape 范围
      │  TensorRT Runtime

GPU 上跑推理

TensorRT 与 PyTorch 的关键差异在于"编译 vs 解释":PyTorch 默认是 eager 模式(一行行解释执行),TensorRT 是 AOT(ahead-of-time)编译,构建期把整张图看一遍、合并算子、挑最快 kernel、写死最优执行计划。在 ResNet-50 / BERT 这类经典模型上,相对 PyTorch eager 通常能拿到 2–5× 的延迟降低;在 LLM 上结合 TensorRT-LLM(见 TensorRT-LLM)能进一步突破。

要理解 TensorRT 的位置,把它放到推理栈里看(详见 推理路径全景GPU 优化原理):上层是训练框架,下层是 CUDA Driver,TensorRT 在中间做"框架无关的 GPU 推理编译器"。

二、核心机制:Builder → Engine → Runtime

TensorRT 的工作流分三个阶段,理解这三个阶段就理解了 TensorRT 的全部心智模型。

1. Builder:编译期优化的"作坊"

Builder 接收网络定义,做四件事:

优化手段做什么典型收益
图优化算子融合(Conv+BN+ReLU、Linear+ReLU)、常量折叠、死代码消除减少访存、减少 kernel launch 开销
Kernel auto-tuning对每个算子从 kernel 库中挑若干候选,在目标 GPU 上实测选最优同一算子不同 batch/shape 选不同实现
精度校准FP32 → FP16 / INT8 / FP8,配 calibration 或量化感知2–4× 吞吐
Workspace 规划给每个算子分配临时显存 workspace拿空间换速度

图优化细节详见 图优化原理算子融合。一个典型融合:

原始图:   MatMul → Bias → ReLU → MatMul → Bias → ReLU
优化后:   [FusedLinearRelu] → [FusedLinearRelu]   (两次 kernel launch)

2. Engine:编译产物

Builder 输出的二进制叫 Engine(序列化后是 .plan / .engine 文件)。Engine 与 GPU 架构绑定——在 A100 上 build 的 engine 不能直接拿到 H100 上跑(除非用 --useUnifiedCapture 或重新 build)。这一点是初学者最常踩的坑:CI 构建机和部署机不同型号时,必须在部署机重新 build,或者用容器统一镜像。

Engine 不可跨架构迁移

不要把本地 RTX 4090 上 build 的 plan 直接推到 A100 服务器跑。常见做法:

  1. 在与生产同型号的卡上 build,把 plan 文件随镜像发布;
  2. trtexec --saveEngine 在部署机上 build;
  3. 容器内 build 后随镜像分发(推荐,但要求镜像里的 driver/CUDA 版本对齐)。

3. Runtime:执行期

Runtime 加载 Engine,创建 ExecutionContext,绑定输入输出 buffer,调用 enqueueV2 / execute_v3 执行推理。Context 是有状态的对象——同一 engine 可以创建多个 context 并行跑(受 GPU SM 资源约束),这是 Triton 中"concurrent model execution"的底层基础(见 Triton 推理服务)。

python
import tensorrt as trt
import pycuda.driver as cuda  # 或 cuda-python

runtime = trt.Runtime(trt.Logger())
with open("resnet50.engine", "rb") as f:
    engine = runtime.deserialize_cuda_engine(f.read())

context = engine.create_execution_context()
# 分配输入输出 device buffer、绑定时需要 name → buffer 地址

三、INT8 量化与 Calibration

INT8 是 TensorRT 的招牌能力之一。详见 量化权重-激活混合精度,这里聚焦 TensorRT 的实现路径。

PTQ 路径:Entropy Calibration

TensorRT 用 IInt8EntropyCalibrator2 做 PTQ(post-training quantization):

  1. 准备几百张代表输入(calibration dataset);
  2. Builder 用 FP32 跑这些输入,收集每一层激活的分布;
  3. 对每层选一组 (scale, zero_point),使得量化前后 KL 散度最小(hence "entropy");
  4. 把 scale 写入 engine,推理时按 INT8 计算。
python
import tensorrt as trt

# 自定义 calibrator
class MyCalibrator(trt.IInt8EntropyCalibrator2):
    def __init__(self, data_loader, cache_file="int8.cache"):
        super().__init__()
        self.data_loader = data_loader
        self.cache_file = cache_file
        self.batch_iter = iter(data_loader)
        self.device_input = cuda.mem_alloc(...)  # 按 batch 大小分配

    def get_batch(self, names):
        try:
            batch = next(self.batch_iter)
        except StopIteration:
            return None
        cuda.memcpy_htod(self.device_input, batch.numpy())
        return [int(self.device_input)]

    def read_calibration_cache(self):
        if os.path.exists(self.cache_file):
            with open(self.cache_file, "rb") as f:
                return f.read()

    def write_calibration_cache(self, cache):
        with open(self.cache_file, "wb") as f:
            f.write(cache)

builder.int8_calibrator = MyCalibrator(...)
builder.config.set_flag(trt.BuilderFlag.INT8)

QAT 路径:从 PyTorch 量化感知训练导入

如果 PTQ 掉点严重(<1% 也算严重),就走 QAT:在 PyTorch / pytorch-quantization toolkit 里训练带伪量化算子的模型,导出 ONNX 时带 QuantizeLinear/DequantizeLinear 节点,TensorRT 8.x+ 会识别并直接生成 INT8 engine。QAT 通常精度比 PTQ 高 0.3–1 个点,但训练成本高。

精度阶梯

精度显存吞吐精度损失何时用
FP320验证基线
FP160.5×~2×<0.1%几乎总是开
INT8 (PTQ)0.25×3–4×0.3–1%检测/分类主流
INT8 (QAT)0.25×3–4×<0.3%精度敏感场景
FP8 (H100+)0.25×4–6×<0.5%大模型新宠

四、Plugin 接口:自定义算子的逃生通道

模型里有 TensorRT 不认识的算子怎么办?两条路:

  1. 改写模型:用等价的、TensorRT 支持的算子组合替代(推荐,可维护性好)。
  2. 写 Plugin:实现 IPluginV2 / IPluginV2DynamicExt / IPluginV3 接口,注册到注册表,TensorRT build 时按名字解析。
cpp
// 简化的 Plugin 接口
class CustomLayerPlugin : public nvinfer1::IPluginV2DynamicExt {
    const char* getPluginType() const noexcept override { return "CustomLayer"; }
    int enqueue(...) noexcept override {
        // 调用自定义 CUDA kernel
        custom_layer_cuda_forward(...);
        return 0;
    }
    // ... clone / serialize / getOutputDimensions 等
};

REGISTER_TENSORRT_PLUGIN(CustomLayerPlugin);

Plugin 的存在让 TensorRT 既能"优化标准算子",又能"逃出去做不标准的事"——这是它兼容性兜底的最后一道防线。代价是 Plugin 算子不参与 graph optimization(无法与相邻算子融合),性能通常低于原生算子。

Plugin 的取舍

能用标准算子组合表达就别写 Plugin。维护成本高、build 链路复杂、跨版本 API 不稳定(V2 → V2DynamicExt → V3 经历过若干 breaking change)。但有些自定义 attention / 投影层确实只能靠 Plugin,掌握它仍然是工程必备。

五、与 PyTorch 的关系:torch_tensorrt

直接走 ONNX → TensorRT 的链路繁琐:导出 ONNX、检查 opset 支持度、调 plugin。torch_tensorrt 是 NVIDIA 推出的"直连"方案——直接吃 torch.nn.Moduletorch.export 图,内部用 Dynamo/TorchScript 编译到 TensorRT,未覆盖的算子回退到 PyTorch eager(hybrid graph)。

python
import torch_tensorrt

model = model.eval().cuda()
inputs = [torch.randn(1, 3, 224, 224).cuda()]

trt_model = torch_tensorrt.compile(
    model,
    ir="torch_compile",            # 新链路,基于 Dynamo
    inputs=inputs,
    enabled_precisions={torch.float, torch.half},  # 开 FP16
    workspace_size=1 << 30,
    min_block_size=5,              # 子图最小节点数,小于这个回退 eager
)

# 之后 trt_model 跟原模型一样调用
out = trt_model(inputs[0])

torch_tensorrt 1.x 起以 ir="torch_compile" 为推荐路径,与 PyTorch 2.x torch.compile 生态对齐——这是 2024 年后 PyTorch 推理加速的事实标准。详见 图优化原理

六、性能数据:基线参考

下面给一组基线数字,仅作"量级感"参考,实测请用你自己的模型 + 卡(基准方法论见 基准测试):

模型硬件精度延迟(batch=1)相对 PyTorch eager
ResNet-50A100 80GFP16~0.4 ms3–4×
BERT-baseA100 80GFP16~1.0 ms
ResNet-50T4INT8~1.0 ms5–6×
YOLOv8-mA100FP16~2.5 ms
ViT-B/16A100FP16~2.0 ms3–4×

基线数字的陷阱

任何"TensorRT 比 PyTorch 快 X 倍"的数字都受以下因素影响:

  1. PyTorch 基线是否开 AMP / torch.compile:开 torch.compile 后差距通常缩到 1.3–1.8×;
  2. batch size:大 batch 下差距收窄(瓶颈转向显存带宽);
  3. shape 是否动态:动态 shape 下 TensorRT 优化空间被压缩;
  4. 是否预热:第一次推理含 CUDA 上下文初始化,必须排除。

七、局限与边界

TensorRT 不是万能的。它的"边界"来自设计取舍:

  1. GPU 架构绑定:Engine 不可跨架构迁移,CI/CD 流水线必须按目标卡 build;FPGA、AMD GPU、Apple Silicon 完全不支持——这种场景考虑 ONNX RuntimeOpenVINO
  2. 动态 shape 麻烦:支持但要预声明 OptProfile(min/opt/max),超出范围会报错或性能骤降。LLM 这种 seq_len 高度动态的场景,TensorRT-LLM 用 "Paged KV Cache + In-Flight Batching" 解决(见 TensorRT-LLM)。
  3. 算子覆盖不全:模型带新算子或非标准结构(如某些 attention 变体)时容易踩"unsupported op",要么写 Plugin、要么改模型。
  4. 构建时间长:Builder 的 kernel auto-tuning 很贵,大模型 build 一次几小时到几十小时。CI 必须用 plan 缓存。
  5. 闭源:开源替代(TVM、MLC、TensorRT 的开源部分 ONNX Parser)成熟度仍有差距。
  6. 学习曲线高:API 在 7.x/8.x/8.6/10.x 间多次 break,文档与版本对应关系复杂。

八、与同类方案的关系

方案定位关系
ONNX Runtime跨平台通用引擎(见 ONNX RuntimeORT 有 TensorRT Execution Provider——ORT 是上层,TensorRT 是 backend 之一
OpenVINOIntel CPU/iGPU 优化(见 OpenVINO同样是"图优化 + 量化"思路,但目标硬件不同
torch.compile / inductorPyTorch 原生 AOT 编译与 TensorRT 形成"原生 vs 旗舰"两条路,torch_tensorrt 把两者打通
TensorRT-LLMLLM 专用扩展在 TensorRT 之上做 PagedAttention、In-Flight Batching、continuous batching 等 LLM 优化(见 TensorRT-LLM
vLLM / SGLangLLM 推理引擎(见 vLLM互补而非替代——TensorRT-LLM 在 NVIDIA 栈里对位 vLLM

九、可继续追踪

参考资料