跳到主要内容

L4.4 TensorRT-LLM 与 ONNX Runtime

三维坐标 layer: L4(推理 Serving)level: Seniorpillar: 训推框架

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.13torch.onnx.export(dynamo=True) 已是官方推荐导出路径。本篇只锚定「版本线」,具体小版本号与参数拼写以官方 release notes 与 quickstart 为准

背景与现状

三个栈的分工,先用一张表立住:

强项弱项
vLLM开源、PagedAttention、连续 batching、生态迭代快NVIDIA 以外硬件支持相对弱
TensorRT-LLMFP8/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「静态」的第一层含义
  • dtypefloat16 / bfloat16 / fp8 / int4_awq(FP8 需 Hopper/Ada,衔接 L4.2 的量化路线)
  • use_gpt_attention_plugin:融合 attention kernel
  • use_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 BatchingTRT-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 选型

维度vLLMTensorRT-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_inferenceonnxruntime.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硬件
CUDAExecutionProviderNVIDIA GPU
TensorrtExecutionProviderTRT 加速 ORT 子图
CPUExecutionProvider通用 fallback
OpenVINO / QNN / CoreMLIntel / 高通 / 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 规则正是为其设计)。

踩坑预警

  1. ONNX opset 过低:新算子 export 失败,优先 opset ≥17。
  2. 动态轴与 TRT:TRT EP 对 dynamic shape 支持有限,生产常固定 max shape。
  3. TRT engine 不可跨 GPU 架构迁移:CI 需按目标 SM 分别 build(见 2.1 节业务症状)。
  4. export 含 Python control flow:需 torch.onnx.export(..., dynamo=True)(PyTorch 2.5+)或重写为静态图。
  5. EP 静默回落:声明了 TRT/CUDA EP 不等于真在用,务必核对实际分派(见 2.4 节)。
  6. 导出即验数值: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):

  1. 内部流量:合同摘要场景,输入恒为「模板 + 文档」≈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 的加速全是净赚。
  2. 公网流量:自由对话,长度长尾分布,P99 要求 max_seq=4096,预留 KV = 0.5 MB × 4096 × 32 = 64 GB;但均值只有 ≈400 token,实际占用 ≈6.25 GB——利用率 ≈10%。为了 1% 的长请求,90% 的 KV 预算在陪跑,同样的卡 vLLM 能塞下数倍并发。
  3. 两个场景 kernel 加速比相同(设 +30%),但第 2 个场景并发容量先崩了——吞吐瓶颈从算子效率转移到 KV 显存周转,这正是 L4.1 PagedAttention 要解的问题。

所以边界不在「模型多大」而在「shape 分布多齐」:P50 与 P99 越接近,越该 TRT-LLM;长尾越重,越该 vLLM(或按长度分桶拆多个 engine,用运维复杂度换回利用率)。

思考题 2:ONNX 导出图断裂与精度漂移的排查决策树

同事把一个带 if 分支和自定义 CUDA 算子的模型导出 ONNX:先是 export 直接报错,改了参数后能导出了,上线又发现 fp16 分数与 PyTorch 对不齐。结合 2.3 节,给出一棵「导不出 → 导出了跑不快 → 跑得快但不对」的三层排查决策树,并算一遍 fp16 归约误差有多凶。

展开参考答案(含三层排查决策树图 + 算一遍)

结论:ONNX 导出问题按「图完整性 → EP 分派 → 数值对齐」三层递进排查——control flow 断图用 dynamo=True 导出解决,自定义算子靠注册 symbolic 或接受 CPU fallback,而 fp16 精度漂移的根因多半是 Cast 插入位置改变了归约的累加精度,需逐层比对定位后对关键子图强制 fp32。

算一遍(fp16 长归约有多凶):把 4096 个 0.1 逐个用 fp16 累加(模拟 seq=4096 的 mean/sum 归约留在 fp16):

累加精度结果真值 ≈409.6相对误差
fp32 累加409.5✅ 基本精确≈0%
fp16 逐项累加256.0❌ 严重偏低约 -37%

fp16 的尾数只有 10 位,累加和到 256 之后,再加 0.1 的增量小于最低有效位,直接被吞掉——和就此「饱和」在 256.0(numpy 可复现)。真实模型里偏差不会这么极端(值有正负、有 blocked reduction),但千分之几的系统性漂移足以让排序/打分业务 AB 判负,这就是 2.3 节业务症状里「Cast 插入位置不同」的数值根源。排查心法:先保图完整,再保 EP 不回落,最后用逐层比对钉死首个数值偏差节点——顺序反了就会在错误的层白耗时间。

思考题 3:三路部署的性能-运维成本算总账

同一 7B 模型要扛 QPS 100、平均输出 300 token 的线上流量(NVIDIA 卡)。结合 2.1–2.2、2.4 节,从「卡数、build/发布成本、回退灵活度」三本账对比 TRT-LLM / vLLM / ORT 三条路径——什么条件下 TRT-LLM 省下的卡钱会被运维成本吃回去?

展开参考答案(含三路 TCO 对比图 + 算一遍)

结论:TRT-LLM 用编译红利省卡(本例每月约省 8000 美元),但每次基座升级都要为每种 GPU 架构重付 build 税;权重季度级更新时这笔账稳赚,周级更新时省下的卡钱会被 build 流水线与发布风险吃回去,而 ORT 在纯 GPU 服务端吞吐不占优、真正价值在跨硬件与端侧,不该进这道服务端吞吐题的决赛。

算一遍(吞吐数字为示意口径,比例参考公开 benchmark 量级,实测以自家压测为准):

  1. 需求:QPS 100 × 300 token = 30000 token/s 稳态生成吞吐。
  2. 卡数:设单卡 vLLM 1500 token/s、TRT-LLM(FP8 + 融合 kernel,+30%)1950 token/s → vLLM 需 ⌈30000/1500⌉ = 20 张,TRT-LLM 需 ⌈30000/1950⌉ = 16 张。按每卡月租 2000 美元:40000 美元 vs 32000 美元,TRT-LLM 每月省 8000 美元
  3. build 税:若基座权重周级更新、线上有 A100/L40S 两种架构,每周重 build 2 次 × 约 3 小时,加上验证与制品分发,每月固定吃掉数十工程师小时;再算上 2.1 节业务症状那种「build 阻塞发布窗口」的风险成本——当模型迭代快到周级,8000 美元/月的卡钱红利大概率被吃平甚至倒贴。权重季度级 frozen 时,同一笔 build 税摊薄到忽略不计,TRT-LLM 稳赚。
  4. 回退灵活度:vLLM 出问题可分钟级回滚镜像或切换模型;TRT-LLM 回滚依赖「上一版 engine 制品还在且兼容」;ORT 的角色是第三本账——它在这道纯 NVIDIA 服务端吞吐题里不进决赛,但当你还要覆盖 CPU 集群兜底或端侧 NPU 时,同一份 ONNX IR 的可移植性是另两条路径给不了的

一句话收束:卡钱账算的是吞吐差 × 规模,运维账算的是更新频率 × 架构种类——两本账相除得到的「盈亏平衡更新周期」,才是 TRT-LLM vs vLLM 的真边界;ORT 则根本不在这条边界上,它解决的是「NVIDIA 之外」的问题。

延伸阅读

1. 本站关联篇目

2. 官方文档(版本口径以此为准)

3. 工具与源码必读路径

  • Polygraphy(TensorRT 工具箱)— 逐层数值比对与图诊断,思考题 2 排查链路的主力工具。
  • NVIDIA/TensorRT-LLM 仓库 — 看 tensorrt_llm/builder.py 理解 build 期做了哪些决策;examples/ 下各模型的 checkpoint 转换脚本是理解权重映射的最短路径。
  • microsoft/onnxruntime 仓库 — 看 onnxruntime/core/optimizer/ 下的 graph transformer 实现,理解 2.3 节各类图手术在代码里的样子。

一句话带走:vLLM、TRT-LLM、ORT 三条路径的分野,归根结底是**「在哪个阶段付出优化成本」**——运行时(灵活)、编译期(极致)、还是 IR 层(可移植)。选型时先画流量画像、再算两本账(思考题 1、3),最后别忘了数值对齐是所有路径共同的底线(思考题 2)。