L4.4 TensorRT-LLM 与 ONNX Runtime
三维坐标
layer: L4(推理 Serving)|level: Senior|pillar: 训推框架L4.1 以 vLLM 为主讲 Serving 调度;L4.2 讲量化。本章补齐 NVIDIA 深度绑定栈(TensorRT-LLM,本体 Apache-2.0 开源、底层 TensorRT kernel 闭源) 与 跨框架部署栈(ONNX + ORT)——含 build 链路、图手术与最小可运行示例。三条路径不是 「谁取代谁」,而是三种不同的编译-运行时权衡:vLLM 用动态调度换灵活,TRT-LLM 用离线编译换极致 kernel,ONNX/ORT 用标准 IR 换硬件可移植。
学习目标
- 前置知识:读过 L4.1–L4.2;知道 transformer 推理分 prefill/decode。有 NVIDIA GPU 可跑 TRT-LLM;无 GPU 可完成 ONNX 导出 ORT CPU 路径。
- 学完产出:① 能描述 TensorRT-LLM build → engine → serve 三阶段及与 vLLM 的定位差异;② 能解释 Inflight Batching 与 Continuous Batching 的异同;③ 能完成 PyTorch → ONNX 导出并用 ORT 推理;④ 能列举两类常见 graph surgery(算子融合前常量折叠、动态轴固定);⑤ 能为「低延迟 NVIDIA 独占」vs「跨硬件/边缘 ORT」做选型,并算清三路部署的卡数与运维成本账。
- 阅读姿势:盯住一条主线——运行时优化本质是编译问题。export 时的 opset、动态轴、精度与 EP 选择,决定了同一模型 30–50% 的性能差距;而「编译得越死,跑得越快、改得越痛」这条守恒律,贯穿本篇所有选型结论。
📅 时效口径(2026-07):TensorRT-LLM 已进入 1.x 正式版线(PyPI 当前处于 1.2 线,早期 0.x 教程的 API/参数命名已有较大变化);ONNX Runtime 处于 1.27 线并保持月度发布节奏;PyTorch 稳定线为 2.13,
torch.onnx.export(dynamo=True)已是官方推荐导出路径。本篇只锚定「版本线」,具体小版本号与参数拼写以官方 release notes 与 quickstart 为准。
背景与现状
三个栈的分工,先用一张表立住:
| 栈 | 强项 | 弱项 |
|---|---|---|
| vLLM | 开源、PagedAttention、连续 batching、生态迭代快 | NVIDIA 以外硬件支持相对弱 |
| TensorRT-LLM | FP8/INT4 专用 kernel、激进层融合、NVIDIA 上峰值最优 | build 慢、新模型支持滞后、底层 kernel 闭源 |
| ONNX Runtime | 跨 CPU/GPU/NPU/边缘、标准 IR、部署面最广 | LLM 自回归链路需手动拆图或 optimum 辅助 |
产业分工大致稳定:云厂商的低延迟 API 后端常见 TRT-LLM 或定制 CUDA 栈(权重 frozen、SLA 严苛、愿意为 build 流水线买单);通用推理平台与快速迭代业务用 vLLM;端侧与多硬件覆盖用 ORT + QNN/CoreML/OpenVINO EP。
业界信号:TensorRT-LLM 进入 1.x 后重心转向 PyTorch-native 工作流(LLM API 直接吃 HF 权重,弱化手写 checkpoint 转换),说明 NVIDIA 自己也承认「build 链路太重」是最大采用阻力;而 ONNX 阵营靠 Phi/Llama 系列官方 ONNX 权重 +
onnxruntime-genai补齐 LLM 自回归短板。两边都在向「vLLM 式易用性」靠拢——但编译换性能的本质权衡没有消失,只是被工具链藏得更深。
原理与架构
2.1 TensorRT-LLM 构建链路
TRT-LLM 的核心思想是把推理当成编译目标:离线阶段做完所有昂贵决策(kernel 选型、层融合、精度分配、shape 范围绑定),运行时只做纯执行。
Build 关键参数:
max_batch_size/max_input_len/max_output_len:决定 engine 显存预算与合法 shape 范围——超范围的请求直接被拒,这是 engine「静态」的第一层含义dtype:float16/bfloat16/fp8/int4_awq(FP8 需 Hopper/Ada,衔接 L4.2 的量化路线)use_gpt_attention_plugin:融合 attention kerneluse_inflight_batching:服务侧动态 batch(TRT-LLM 对 continuous batching 的实现名,语义同 L4.1:请求粒度进出 batch,不等整批结束)
Build 一次可耗时 数十分钟(7B 模型),且 engine 与 GPU 架构绑定(SM 8.6 上 build 的 engine 不能在 SM 9.0 上跑)——这两条共同决定了 TRT-LLM 的运维形态:engine 是制品(artifact),必须进 CI/CD 流水线管理,而不是启动时现场生成。
真实业务症状:某团队把基座模型从 7B 升级到 8B 新版本,发布日当天才发现 TRT engine 需要重 build——A100 与 L40S 两种卡各 build 一遍共耗时 3 小时,直接撑爆当晚的发布窗口,最后回滚改期。复盘结论:engine build 必须前置到 CI(模型权重合入即触发按 SM 架构矩阵 build),发布窗口只做「拉取已验证 engine + 切流」;vLLM 路线则没有这个问题,代价是峰值吞吐让一截。这就是 2.2 节选型表里「启动速度」一行背后的真实运维含义。
Inflight Batching 与 Continuous Batching 是一回事吗? 语义上同源——都指「请求粒度动态进出 batch,不等整批跑完」,但实现底座不同:
| 维度 | vLLM Continuous Batching | TRT-LLM Inflight Batching |
|---|---|---|
| KV 管理 | PagedAttention 按块动态分配 | engine 预留 max shape 显存池内周转 |
| shape 约束 | 无静态上限(受总显存约束) | 受 build 时 max_* 参数硬约束 |
| 调度器位置 | Python/引擎层,可改可插拔 | C++ Batch Manager,配置驱动 |
| 与量化 kernel 协同 | 通用 kernel + 量化后端 | 融合进 build 期 kernel 选型,耦合更深 |
理解这张表就理解了两个栈的「性格」:vLLM 把灵活性放在运行时,TRT-LLM 把性能锁在编译期——后面所有选型差异都是这一句的展开。
2.2 vLLM vs TensorRT-LLM 选型
| 维度 | vLLM | TensorRT-LLM |
|---|---|---|
| 启动速度 | 快(免 build,分钟级换模型) | 慢(需 build engine,小时级) |
| 峰值吞吐(NVIDIA) | 很高 | 常更高(尤其 FP8/INT4 kernel) |
| shape 灵活性 | 动态(PagedAttention 按需分块) | 静态范围(build 时锁定 max shape) |
| 多 LoRA | 原生支持 | 需 plugin 配置 |
| 开源/可改 | 完全开源 | 本体开源、核心 kernel 闭源 |
| 典型场景 | 通用 API、快速迭代、长尾流量 | 生产 SLA、权重 frozen、成本极致 |
一句话判据:模型改得越勤、流量形状越散,越该 vLLM;权重越稳、流量形状越齐、NVIDIA 卡越统一,TRT-LLM 的编译红利越能兑现。定量边界见思考题 1 与思考题 3。
2.3 ONNX 导出与 Graph Surgery
ONNX 是「训练框架 → 部署运行时」之间的标准中间表示(IR):PyTorch 把计算图导出成一份与框架无关的 protobuf,之后的优化、量化、跨硬件分派都在这份 IR 上进行。整条链路:
导出:
import torch
import torch.nn as nn
class TinyLM(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(32000, 256)
self.linear = nn.Linear(256, 32000)
def forward(self, input_ids):
x = self.embed(input_ids)
return self.linear(x.mean(dim=1))
model = TinyLM().eval()
dummy = torch.randint(0, 1000, (1, 16))
torch.onnx.export(
model, dummy, "tiny_lm.onnx",
input_names=["input_ids"],
output_names=["logits"],
dynamic_axes={"input_ids": {0: "batch", 1: "seq"}, "logits": {0: "batch"}},
opset_version=18,
)
依赖:pip install torch onnx onnxscript onnxruntime(PyTorch 2.5+ 导出 ONNX 需要 onnxscript;2.13 线上 dynamo=True 导出路径已成熟,遇到 control flow 断图优先走它,见思考题 2)。
Graph Surgery 常见操作:
| 操作 | 目的 |
|---|---|
| 常量折叠 | 去掉推理时不变的 subgraph |
| 算子替换 | LayerNorm → 融合版本 |
| 动态轴固定 | 边缘部署固定 batch=1, seq=128 以启用更多 fusion |
| 外部数据分离 | 大权重 external_data 便于 mmap |
工具:onnx.shape_inference、onnxruntime.transformers.optimizer、Polygraphy。
真实业务症状:某团队把检索排序模型导出 ONNX 上线后,线上分数与 PyTorch 离线分数出现约千分之三的系统性偏差,AB 实验直接判负。排查两天,最终定位是 fp16 下 Cast 插入位置不同:PyTorch 侧在
mean归约前把中间量升回 fp32,而 ORT 优化器融合后整段归约留在 fp16 做——低精度长归约的累加误差被放大(见思考题 2 的算一遍)。修法:用 Polygraphy 逐层比对定位首个偏差节点,对该归约子图强制 fp32(ORT 转换工具的 op 级精度黑名单 / 手动插 Cast)。教训:导出后的数值对齐验证(np.allclose分层比对)必须是上线 checklist 的一项,而不是出了事故再补。
2.4 ORT Execution Provider
ORT 的可移植性来自 EP(Execution Provider)抽象:同一张 ONNX 图,按 EP 能力切成子图分派给不同硬件后端,跑不了的节点回落 CPU。
| EP | 硬件 |
|---|---|
| CUDAExecutionProvider | NVIDIA GPU |
| TensorrtExecutionProvider | TRT 加速 ORT 子图 |
| CPUExecutionProvider | 通用 fallback |
| OpenVINO / QNN / CoreML | Intel / 高通 / Apple |
工程要点:EP 是按优先级列表声明的(如 ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]),一旦图里有 TRT/CUDA 不支持的算子,该子图会静默回落到下一级 EP——性能掉了但功能正常,是最隐蔽的性能退化来源。上线前务必确认实际生效的 EP 分派,而不是只看你声明了什么:
import onnxruntime as ort
so = ort.SessionOptions()
so.log_severity_level = 1 # verbose:日志会打印每个子图分派到哪个 EP
sess = ort.InferenceSession(
"tiny_lm.onnx", sess_options=so,
providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
print(sess.get_providers()) # 实际可用的 EP 列表(声明了但环境缺库会被剔除)
若日志里出现大量节点落在 CPUExecutionProvider,说明图被切碎、GPU 与 CPU 之间来回搬数据——此时优先做 2.3 节的算子替换/图手术,把不支持的算子换成 EP 认识的等价形式,而不是硬调 batch 参数。
动手实践:ONNX 导出与 ORT 推理对比
A. ONNX 导出 + ORT 推理(CPU/GPU 均可)
pip install torch onnx onnxruntime
python export_tiny_lm.py # 上文脚本存为文件
# ort_infer.py
import numpy as np
import onnxruntime as ort
sess = ort.InferenceSession("tiny_lm.onnx", providers=["CPUExecutionProvider"])
input_ids = np.random.randint(0, 1000, (2, 16), dtype=np.int64)
out = sess.run(None, {"input_ids": input_ids})
print("logits shape", out[0].shape)
B. TensorRT-LLM 最小 build(需 NVIDIA GPU + 容器)
# 参考 NVIDIA NGC 容器 trtllm-python-py3
# 以官方 quickstart 为准,版本随 release 变化
pip install tensorrt_llm -U --pre --extra-index-url https://pypi.nvidia.com
# 转换 + build(示意,模型需从 HF 下载)
python convert_checkpoint.py --model_dir ./llama-2-7b-hf --output_dir ./trt_ckpt
trtllm-build --checkpoint_dir ./trt_ckpt \
--output_dir ./trt_engines \
--gemm_plugin float16 \
--max_batch_size 8 \
--max_input_len 1024 \
--max_output_len 256
# 推理 smoke
python run.py --engine_dir ./trt_engines --max_output_len 32
6GB 显存提示:7B 模型 TRT-LLM 需量化(INT4/AWQ)或更小模型(TinyLlama 1.1B)做 smoke;与 L4.2 AWQ 章节配合。
C. Graph 优化(ORT Transformer 优化器)
pip install onnxruntime-tools
python -m onnxruntime.transformers.optimizer --input tiny_lm.onnx --output tiny_lm_opt.onnx
对比优化前后 ORT 推理延迟(time.perf_counter 循环 100 次),并顺手做一次数值对齐检查:同一输入分别过 PyTorch 与优化后 ONNX,np.allclose(atol=1e-3) 不过就先别谈性能。
建议把三组结果记成一张表(示意格式,数值以你的机器实测为准):
| 配置 | 100 次平均延迟 | 相对基线 | 数值对齐(atol=1e-3) |
|---|---|---|---|
| PyTorch eager(基线) | — | 1.00× | — |
| ONNX + ORT(原始图) | — | 常见 0.6–0.9× | ✅ 应通过 |
| ONNX + ORT(optimizer 优化后) | — | 常见 0.5–0.8× | ✅ 不通过则回查 Cast |
TinyLM 太小,优化收益不明显属正常——这个 Lab 的目的在跑通「导出 → 优化 → 验证对齐 → 测延迟」的完整流程,真实收益要在带 attention 的 transformer 模型上才显现(onnxruntime.transformers.optimizer 的 fusion 规则正是为其设计)。
踩坑预警
- ONNX opset 过低:新算子 export 失败,优先 opset ≥17。
- 动态轴与 TRT:TRT EP 对 dynamic shape 支持有限,生产常固定 max shape。
- TRT engine 不可跨 GPU 架构迁移:CI 需按目标 SM 分别 build(见 2.1 节业务症状)。
- export 含 Python control flow:需
torch.onnx.export(..., dynamo=True)(PyTorch 2.5+)或重写为静态图。 - EP 静默回落:声明了 TRT/CUDA EP 不等于真在用,务必核对实际分派(见 2.4 节)。
- 导出即验数值:fp16 下 Cast 插入位置不同足以让分数系统性漂移(见 2.3 节业务症状),对齐验证进 checklist。
配套代码:ai-infra-labs
export_tiny_lm.py+ort_infer.py— CPU/GPU 均可跑通 ONNX→ORT 闭环;TensorRT-LLM build 需 NGC 容器 + 足够显存。
深入思考
下面三题每题先给题干,再用
<details>折叠一份图文并茂的参考答案。建议 先合上答案自己想 3 分钟,再展开对照。
思考题 1:静态 engine vs 动态调度的适用边界
TRT-LLM 的 engine 在 build 时锁死 max_batch_size / max_input_len / max_output_len,vLLM 则靠 PagedAttention 按需分块。结合 2.1–2.2 节:为什么「企业内部固定模板流量」是静态 engine 的主场,而「公网长尾流量」会让同一套配置的性价比崩塌?请用 KV 显存利用率算出这条边界。
展开参考答案(含流量形状-引擎选型决策图 + 算一遍)
结论:静态 engine 的性价比取决于「真实流量 shape 与 build 时锁定的 max shape 的贴合度」——内部固定模板流量贴合度可达 90% 以上,编译红利全额兑现;公网长尾流量贴合度常跌到 10% 上下,为长尾预留的 KV 显存大量空转,此时 vLLM 的按需分块反而是更便宜的选择。
算一遍(7B 级模型,fp16 KV 约 0.5 MB/token,batch=32):
- 内部流量:合同摘要场景,输入恒为「模板 + 文档」≈2048 token、输出 ≈128 token,engine 按
max_input_len=2048, max_output_len=256锁定,预留 KV = 0.5 MB × 2304 × 32 ≈ 36 GB;实际每请求 ≈2176 token,实际占用 ≈34 GB——利用率 ≈94%,预留几乎不浪费,融合 kernel 的加速全是净赚。