
最近被问到一个很直接的问题TensorRT 不是已经在 NVIDIA GPU 上把推理压榨到接近极限了吗为什么还要做 MLIR-TensorRT 这种听起来像“底层套底层”的东西我反手抛了个问题回去如果你的模型里同时有 PyTorch 导出的子图、ONNX 转换过来的子图还有团队自己用 C 堆出来的推理逻辑能不能把它们编到同一份可分析的中间表示里再做统一调度对方愣了一下。在传统 TensorRT 的使用语境下这个问题确实不好答因为 TensorRT 更像一个高吞吐的加速库而不是一座能让不同前端自由汇入的编译桥梁。NVIDIA 做 MLIR-TensorRT本质上就是在把推理编译器这件“工厂流水线”下沉到 MLIR 生态里让 TensorRT 不再是黑盒终端而是一段可被编排、可被改写、可被观察的编译后端。很多做部署优化的同学第一次看到 MLIR-TensorRT 这个项目名都会产生一个疑问它到底是 MLIR 插件还是 TensorRT 的替代品抑或只是 NVIDIA 给 HPC/自动驾驶团队准备的“内部玩具”这篇文章我会从我的实际认知出发把项目背景、核心概念、工作链路、横向对比和上手路径一次讲透。文章不会有太多需要你“闭眼照着做”的 magic command反而会花更多篇幅解释为什么这套设计值得你关注以及哪些地方目前仍然有坑。1. TensorRT 这些年一直是“加速库”为什么现在要谈编译器1.1 加速库的“舒服”是有代价的如果你过去两年一直在用 TensorRT 做推理部署一定熟悉这条流水线把 PyTorch 模型导出 ONNX然后用 trtexec 或者 TensorRT Python API 构建 engine再用 runtime 加载 engine 去执行。这套流程对绝大多数单模型场景都非常省心TensorRT 会帮你做常量折叠、层融合、精度选择、kernel 自动调优甚至量化校准。但它的“省心”其实是建立在几个隐藏前提上的你的模型结构相对完整、你的前端格式足够标准、你只在一个 GPU 环境上长期服役。一旦需求变成“多个模型拼接成一个推理服务”“某个子结构想用自定义 kernel 替换”“想对算子融合规则做细粒度控制”传统 TensorRT 的 API 模型就开始暴露边界。你可以通过 plugin 塞自定义算子也可以把多个 engine 串起来但这些操作更像是在一座已经很满的仓库里硬塞新货架而不是重新设计仓库的货架体系和通行路径。从工程视角看TensorRT 本质上是一个“链接器加运行时”它把网络定义变成可执行的二进制 engine再把输入输出暴露给你。问题不在于它做得不够快而在于它没有给你提供一个足够开放的中间层来做图级别的代码变换。过去大家总说“ONNX 是模型界的通用语言”可实际上 ONNX 在部署链路上更像一个传输格式它适合做模型交换却很难在此基础上推理大规模的组合优化。于是就需要一个更稳定的 IR而 IR 恰恰是现代编译器的基础设施。1.2 一个 IR 能带来的自由可看、可改、可复用如果你熟悉 LLVM会知道 IR 为编译器带来了三个关键能力可视化、可改写、可复用。模型推理也需要类似的东西。当我们把一张推理图从框架里抽出来之后第一步不是急着把它变成 kernel而是先让它进入到一种可以被人和工具共同阅读的表示里。对这张图做算子融合、内存复用和并行调度才算真正进入“编译”环节。MLIR 的厉害之处在于它不是一套固定的 IR而是一套 IR 框架。你可以定义自己的 dialect把 TensorRT 的层、CUDA 的 kernel、显存分配模型都表达成方言里的 op。接着就可以复用 MLIR 生态里已有的通用 pass例如常量传播、死代码消除、循环不变量外提等也可以针对自己的 dialect 写自定义 pass把若干 TensorRT 层融合成复合 op。用生活化的类比来说传统方式是你把一段中文直接交给一位资深翻译让他直接翻成英文翻得又快又好但你无法让另一位编辑在中间阶段对“中文到英文的初稿”做风格调整。MLIR 的逻辑是先翻译成一种规范且可编辑的中间语言再由另一套编译规则把它变成最终机器码。每个环节都能暴露在工具链之下被检查、被优化、被复用这才是编译器思维的真正价值。1.3 “融合 MLIR 生态”不是把 TensorRT 重写一遍NVIDIA 的 MLIR-TensorRT 项目并不是要把 TensorRT 的每一行实现都搬到 MLIR 里然后抛弃原有框架。事实恰恰相反它希望把 TensorRT 放在更加开放的工具链“下游”让 MLIR 生态中的各种前端和优化 pass 都能更顺畅地触达 TensorRT。具体来说有三层含义。第一层输入侧它希望接收来自不同 MLIR 前端的模型表示比如把 StableHLO、TorchScript 或 ONNX 转换到统一的 MLIR 模块里。第二层优化侧图级别的优化和调度不再完全封闭在 TensorRT 内部而是由用户可以看见的 MLIR pass 来完成哪里融合、哪里拆开、保持多少精度都可以插一手。第三层输出侧真正落到 GPU 上执行的仍然是 TensorRT engine 或等价的运行时组件保证 NVIDIA 多年积累的 kernel auto-tuning 和算子库能力不被浪费。一句话说清楚这不是“用 MLIR 重写 TensorRT”而是“把 TensorRT 纳入到一个更大的编译器体系里”。这样既保住了 NVIDIA 在底层 kernel 上的积累又把优化过程的解释权交还给开发者。听上去确实很理想但也要理性看待它目前的成熟度。2. Dialect、Pass、Lowering进入 MLIR-TensorRT 前需要先补的底子2.1 Dialect 就像同一份代码在不同圈子里说不同方言很多第一次接触 MLIR 的人会被 dialect 这个概念挡住。其实它并没有那么玄。如果你去过不同地方的团队开技术评审会会发现大家虽然都在讲“性能优化”这件事但用词完全不同算法工程师说 batch size 和 latency框架工程师说 graph captureCUDA 工程师说 kernel 和 occupancy。MLIR 中的 dialect 本质上就是给 op 定义了不同的“词汇表”。同一个卷积加偏置加 ReLU 的小结构在 ONNX 的表示里可能是Conv - Add - Relu三个 op每个 op 都很干净在 TensorRT 的表示里它可能已经是一个融合过的 “conv bias relu” 复合 op在更接近硬件表示里它可能还要带上 tensor memory accelerator 的布局要求。MLIR 允许这些不同抽象层次的 op 同时存在于同一个模块中并通过 pass 把它们逐层降到更具体的方言上。这样写通用优化的人不必关心硬件细节写硬件细节的人也不必操心前端语义。MLIR 中常见的通用 dialect 其实很多arith负责加减乘除等基础算术tensor描述单个张量值linalg用来表达可复用的算子结构tosa提供一组面向神经网络算子的稳定接口stablehlo则是许多框架前端共同瞄准的表示。MLIR-TensorRT 的价值就是在这个“方言谱系”里加入了自己的方言让模型可以一路顺畅地落到 TensorRT 的地盘上。2.2 Pass 和 Lowering谁在什么时候做改写Pass 是编译器里的“代码改写动作”。最直白的一个例子是算子融合识别并匹配某几个相邻 op把它们合并成一个 op减少中间结果写回显存的开销。还有显存规划把那些生命周期不重叠的 tensor 安排到同一块显存区域从而显著降低总显存占用。MLIR 的标准框架会给你一套 pass 管理机制你可以规定某个 pass 在什么时候跑、跑多少遍、依赖前面哪个 pass 的结果。Lowering 则是从一个 dialect 切到另一个 dialect 的过程。这个过程不是一个 op 对应一个 op 的简单翻译而是要把语义、数据类型、内存布局都对齐。比如把一个tensor表示下很优雅的 element-wise addlower 到带显存 buffer 的形式时就必须考虑这个 buffer 有多大、对齐方式是什么、是不是可以复用前一个 op 的 buffer。不掌握这层思维很容易在看 MLIR 输出时觉得它啰嗦且不直观。完整理解这些概念再去看 MLIR-TensorRT 的工程结构会轻松很多你在命令行工具里看到的那些 pass pipeline、IR dump、内存规划信息其实都是这套编译框架的标准产物并不是 TensorRT 单独给你发明的黑话。2.3 为什么可以用 MLIR 做前端却把 TensorRT 留在内核层稍微有编译器经验的读者可能会问既然我们已经走到 MLIR 了为什么不干脆把算子也生成到 CUDA 代码做一条完全开放的 GPU kernel 编译器答案是成本和风险。模型推理编译器最难的地方并不在“能不能生成一个正确 kernel”而在于要在一个巨大的算子空间里找出每个 shape、每个 GPU、每个精度组合下性能最优的实现。TensorRT 的价值恰恰在于它手里有大量已经自动调优过的 kernel有各种经过验证的融合模板还有对半精度、BF16、INT8、FP8 等细节的长期打磨。从头写一套能跟它掰手腕的 kernel 编译器工程量会非常夸张而且很难在短时间内达到同样高的性能下限。所以 MLIR-TensorRT 的策略很务实图优化、IR 层面的分析和跨模型调度用 MLIR 体系来解决到真正的计算密集算子时仍然调用 TensorRT 或者其背后的 CUDA 内核库。这样既保证了“能看、能改”的灵活性又不牺牲 NVIDIA 在算子上积累多年的性能优势。理解这一点你才不会对这个项目产生不切实际的期待。3. 从 ONNX 到 Plan一条推理模型在 MLIR-TensorRT 里的完整路径3.1 compile 与 runtime一条离线路、一条执行路MLIR-TensorRT 的架构可以粗略分成两部分编译期工具链和运行时组件。编译期那部分负责把前端模型变成一种可执行的“Plan”运行时则负责加载并执行 Plan。在离线路线上你首先需要一个入口把模型读进 MLIR。如果你手里是一个 ONNX 文件可以把它导入成基于 tensor 运算的 MLIR 模块如果你从 PyTorch 导出 TorchScript也可以把 TorchScript 先转成 StableHLO 之类的表示再进入同一条优化管道。拿到 MLIR 模块之后一系列 pass 会开始工作形状推断、转成 TensorRT dialect 或更通用的计算表示、精度规划、算子融合、显存 buffer 规划最后生成一个带完整执行顺序的 Plan。在执行路线上runtime 接收这个 Plan。它不像传统 TensorRT API 那样要求你手握 engine 句柄和 context 手动绑定输入输出而是可以依据 Plan 里的依赖关系完成显存分配、kernel launch、流同步等操作。对多模型联合推理而言这个设计比自己在外面手工拼 engine 要安全得多因为依赖和生命周期都在同一份中间表示里被记录。3.2 TRT Dialect 与 Plan Dialect一个描述“怎么算”一个描述“怎么排”许多刚接触项目的人会混淆两个核心 dialect一个是用来描述 TensorRT 层网络的 dialect另一个是用来描述整个 Plan 的 dialect。你可以把前者理解成“这张图里每个节点计算什么”把后者理解成“这些节点之间的依赖关系、资源分配和执行对象到底是怎样的”。TRT Dialect 会拿着神经网络的核心计算逻辑例如卷积、归一化、全连接、注意力结构等表达成 TensorRT 语义下的 op。这里面的信息足够多可以支撑编译期将图转化成一个或多个 TensorRT engine 表示。Plan Dialect 则更上层一点它描述的是最终运行时如何把这些算子组织起来要用哪几个 CUDA stream、哪些输入输出 tensor 需要做 host 端拷贝、哪个 engine 的 workspace 单独分配、哪个又必须复用上一个 engine 的显存区域。这两层分开之后你可以做很多过去做不了的事单独分析某一段子网络的计算类型、诊断显存碎片、把某个子图替换成自研 plugin并且不会牵扯到其它无关部分。对需要在同一张卡上跑多个模型、共享显存池的服务端场景来说这种“描述与控制分离”的设计比传统 TensorRT 手动管理多个 engine 更干净。3.3 动态 shape、显存规划和多流执行能走到哪一步动态 shape 历来是 TensorRT 使用里最头大的问题之一。过去你用 TRT 时通常要显式指定 optimization profile为不同 batch size 或者输入分辨率单独设范围非常繁琐。MLIR 生态本身对符号 shape 和可变维度的表达有很成熟的支持所以在 MLIR-TensorRT 里一些跟 shape 相关的推断和推理可以被放在更早的阶段完成并且把“到底为哪些 shape 生成 kernel”变成一个显式的策略决策。显存规划在传统部署中往往是个“跑起来才知道”的黑洞。而在 MLIR 工作流里你能看到类似 bufferization 和分配规划留下的痕迹知道当前 Plan 大概需要多少显存、中间的临时 tensor 放在哪里。多流执行也是一样Plan 里可以表达哪些 engine 之间没有数据依赖、可以并行跑在不同 CUDA stream 上调度逻辑不再散落在业务代码里而是下沉到可检查的 IR 里。这些能力听起来非常美好但要注意它们仍然处在迭代过程中并不是每个模块都足够稳定。如果你想在生产环境里直接依赖这套机制处理非常极端的动态场景还是得先做好实验验证和版本锁定。4. 与 XLA、TVM、Torch-TensorRT 横向对照后真正多出来的能力边界4.1 与 XLA 的对照同样都是 MLIR 思想切入角度不同提到 MLIR 和 NVIDIA GPU 推理编译器自然会想到 XLA。XLA 是一个历史悠久、用编译器思维做模型加速的框架尤其跟 TensorFlow 和 JAX 的关系极其紧密。它的前端主要是 HLO/StableHLO优化链路里也大量使用 MLIR最后通过 GPU backend 生成并行计算逻辑。XLA 和 MLIR-TensorRT 的差异与其说是技术栈不同不如说是“目标边界”不同。XLA 更强调从框架前端到 GPU kernel 生成的全链路打通它追求让 JAX/TensorFlow 用户能在不切换工具链的前提下获得较高性能。MLIR-TensorRT 则更像一个面向“终极 N 卡推理性能”的专门通道它不试图用一套纯编译器覆盖所有 GPU 后端而是把大量计算密集算子交给 TensorRT 手调内核因此对单一厂商硬件的优化空间保留得更多。这也就造成了两种完全不同的使用体验用 XLA你通常是在某个框架生态内部完成从 model 到 executable 的构建用 MLIR-TensorRT你更可能是在一个多语言、多框架、多模型的中间地带把它当作统一推理底座的编译器。实际项目里你也完全可以让 JAX 模型的子图先落成 StableHLO再由 MLIR-TensorRT 接手做 NVIDIA GPU 上的极致编译两者并不互斥。4.2 与 TVM / IREE / Torch-TensorRT 的差异TVM 和 IREE 都走“前端多框架、后端多硬件”的路线理念上跟 MLIR-TensorRT 有重叠。TVM 最大的特点是提供了相当完整的 kernel 自动调优和代码生成体系你可以自己写调度模板也可以让 AutoTVM 去搜索配置。IREE 则是 MLIR 社区里很有代表性的推理编译器它非常强调模块化支持多种前端和后端目标是把模型编译成可移植、可嵌入的二进制。MLIR-TensorRT 跟它们最本质的区别在于它不在“通用编译器 任何硬件”这条路上跟所有人竞争。它的目标后端非常明确NVIDIA GPU。它也不追求把所有算子的代码都开放成可以任意修改的源代码因为底层那些融合策略和 kernel 实现许多仍属于 TensorRT 的引擎能力。你可以把 MLIR-TensorRT 看作是“MLIR 的开放外壳 TensorRT 的底层内核”组合体更适合那些硬件路线已经收敛到 NVIDIA、同时又在编译层需要更高可编程性的团队。Torch-TensorRT 是另一个容易混淆的项目。它帮你把 TorchScript 模块里的可优化子图转换到 TensorRT但主要面向 PyTorch 用户做了很多和 TorchScript 编译器对接的工作。MLIR-TensorRT 的前端和 IR 设计则更通用它关心的是模型进入 MLIR 之后怎么被多种工具链共同处理而不是某一个框架的原生格式。4.3 对你作为使用者的直接价值清单横向比较之后我会把 MLIR-TensorRT 的实际收益归结成几条。图级可编程性你能用 pass 和 dialect 来改动图而不是只能依赖 TensorRT 内置的融合策略。多前端统一入口ONNX、TorchScript、StableHLO 等途径都有机会进入同一套 pipeline。多模型与显存的可控性Plan 层面的内存规划和依赖描述对多模型服务更友好。可观察性IR 是可以随时 dump 出来慢慢看的出了问题更有可能定位到具体环节。同时也要记住它的边界它不是一个万能的端到端部署框架算子覆盖范围依然受 TensorRT 支持能力限制如果只是为了把一个成熟单模型做固化拆掉现成方案转投 MLIR-TensorRT 未必划算。5. 上手跑通一个最小推理流程环境、命令与我能预见的坑5.1 环境准备里的第一道坎版本不是“选新的”而是“选配的”想体验 MLIR-TensorRT建议先从官方镜像或与项目 CI 绑定的依赖版本开始而不是自己手动去凑最新的 TensorRT 和 CUDA。原因在于 MLIR 项目对 LLVM 版本的敏感度非常高某个自定义 dialect 可能只兼容特定范围内的 MLIR 接口一旦 LLVM 版本漂移代码可能就编译不过了。我自己的习惯是先检查宿主机 GPU 驱动能用然后在容器里挂载 GPU 跑nvidia-smi确认设备可见。如果你是第一次在干净机器上配环境最大的概率不是 MLIR-TensorRT 本身报错而是驱动与容器运行时对不上导致 TensorRT 找不到设备。遇到这类问题先不要怀疑编译流程先排查驱动版本、容器里能否看到设备以及 TensorRT 库的路径是否被正确加载。从源码构建时我会先把仓库 README 里锁定的 LLVM/MLIR commit 找到再按官方脚本拉取依赖。某些子项目还会带独立的构建脚本不要一股脑全开最好按你需要的那条链路最小化构建。以你拉到的版本为准这一条放到任何时候都适用。5.2 编译一个 ONNX 模型并落地的典型动作动手之前先准备一个小模型比如 ResNet-18 或一个简单的卷积网络导出成 ONNX。通过项目提供的 compile 工具或等价 Python 绑定把 ONNX 导入进来跑一遍 pass pipeline最后生成 Plan。大致的动线是先导入 ONNX 到 MLIR 模块然后跑类型和形状推断确保所有 tensor 的 shape 尽量可判定接着做面向 TensorRT 的算子转换和融合 pass随后做内存规划与依赖排序输出 Plan最后用 runtime 加载 Plan绑定输入输出的 buffer执行推理并验证结果。听起来步骤不算多但真实操作里最需要留意的是“ONNX 里某个算子在 TensorRT 里没有直接对应实现”这种情况。MLIR-TensorRT 一般会尽量把不支持的算子保留在通用 dialect 中后续可能还有机会用自定义 runner 执行但你不能再要求 TensorRT engine 像原生算子那样优化它。项目中不少示例都会展示这种混合执行的能力这是它与传统 TensorRT 的重要区别。5.3 动态 shape 和缓存策略的两个实践提醒第一点关于动态 shape。如果你明确知道服务上线后会遇到几种输入分辨率就不要把动态范围设得过大。范围太宽编译器要为边界形状准备保守策略和更多 kernel 变体容易让显存占用和首次编译时间都显著上升。最好先在符号 shape 层面把边界清晰地表达出来再用少量有代表性的 shape 去验证性能。第二点关于 engine 缓存。MLIR-TensorRT 里的编译过程并不便宜想把编译好的 Plan 或 engine 保存下来复用机制上跟 TensorRT 的 engine 序列化类似但缓存往往会跟 TensorRT 版本、GPU 架构和 CUDA 版本绑定。团队里如果有人升级了驱动或者换了卡先前缓存可能无法加载。建议把缓存作为可再生成的产物而不是不可替代的资产至少在 CI 里留一条“无缓存从零编译”的可执行路径。5.4 遇见报错时的排查材料如果编译过程中遇到算子不支持的报错我会先换一个小而可复现的模型把问题隔离出来再用命令行的 IR dump 开关打印中间结果看是形状推导失败、某 op 没找到对应模式还是类型不被支持。能看懂 MLIR 和 TensorRT 两头的报错是这个项目调试的主要门槛没有什么捷径。启动时若遇到 CUDA 相关的找不到库或初始化失败先跑一遍 TensorRT 自带的样例或trtexec验证底层环境。这样可以把问题边界切清楚是环境问题还是 MLIR-TensorRT 项目本身的问题不要混在一起猜。6. 在业务里引入它之前我的判断标准与现阶段槽点6.1 我的评估清单什么团队适合切进去我不会建议所有做推理部署的团队立刻把管线切到 MLIR-TensorRT但在下面几类痛点上它确实值得认真评估。你有多个模型或多个子图需要在同一 GPU 上协同工作希望更系统地管理显存和流。你觉得 TensorRT 的自动融合策略够强但你想要一些“能自己插入规则”的可编程空间。你需要跨框架PyTorch、JAX、ONNX汇总来源不同的模型而不是只服务于一种前端。你们团队已经有懂 MLIR 或编译器的人愿意投入时间做底层调试而不是只想调用一个黑盒 API。如果没有这些需求只是想把一个稳定模型部署到线上传统 TensorRT 或 PyTorch 自带的导出推理链路可能已经足够没有必要为一个新项目承担额外复杂度。6.2 现阶段几个槽点我不会美化首先项目仍然处在快速演进期。MLIR 接口本身变动频繁MLIR-TensorRT 又紧跟上游一段时间不看就会发现 API 和 pass pipeline 变了。依赖锁定和版本升级会持续消耗维护成本。其次文档和示例虽然不少但比较分散。很多东西需要你把示例代码、官方 issue 和源码实现放在一起看才能真正理解设计意图。对初学者来说学习曲线比传统 TensorRT 陡峭不少不仅要知道模型怎么部署还要明白编译器基本概念。最后算子覆盖边界依然是它绕不开的限制。TensorRT 能做的它大都能做TensorRT 做不了的新算子它只能靠通用 dialect、自定义实现或者回退逻辑去补。也就是说它本身不会凭空扩大 NVIDIA 推理算子的覆盖范围更多是把已有能力放进一个更灵活的编译框架里。6.3 如果想给团队讲清楚这个项目我会用三句话兜底如果要在评审会上用三句话讲清楚它的定位我会这么说第一MLIR-TensorRT 是 NVIDIA 把 TensorRT 能力嵌入 MLIR 生态的一次工程化尝试目标是让图优化过程更开放、更可编程。第二它没有否定 TensorRT 的价值而是把 TensorRT 放在新工具链的最底层保留其内核性能优势。第三它最适合的场景是异构来源、多模型、多 GPU 推理系统的统一编译与调度而不是替代你手里已经跑得很好的单模型部署脚本。从我个人的实际体会看花时间研究 MLIR-TensorRT最大的收获不一定是你马上能把线上延迟降低多少而是你会开始用“编译器”的视角重新审视推理部署。过去那些“黑盒 engine”里令人焦虑的部分——算子怎么融合、内存怎么分配、多个模型怎么互相影响——会逐步变成可以被观察、被操纵、被复用的中间产物。哪怕最终没有全量切换过去这种视角对做高性能推理服务也会有很大帮助。如果你以后真的需要在一个复杂推理系统里打通多个框架、多个模型这套项目理念会是一个非常好的设计出发点。