假设你是一位小说家,写了一部长篇小说(PyTorch 模型),想把它推向全世界——手机、汽车芯片、可穿戴设备就是一个个语言不同的国家。可各国读者的文化习惯、词汇量、语法体系天差地别,怎么办?你找到了 Torch-MLIR 这家“跨国翻译社”。它先为手稿扫描留档(捕获成静态计算图),再让译者直译成包罗万象的“世界语”(torch 方言),原意一字不改,只做格式规范化。接下来,编辑们按各国口味把世界语意译成当地语言(Linalg、TOSA 等底层方言),最后印刷发行(生成目标代码)。更妙的是:无论将来接到多少新语言订单,那份“世界语直译稿”只需准备一次。今天这篇文章,就带你把这家翻译社的流水线从头到尾走一遍。
Torch-MLIR 是 AI 编译器工程师的“跨硬件桥”,掌握它,你才能在模型部署、算子优化、新芯片适配等岗位上真正理解“一次编译、处处运行”的工程智慧。
可视化演示
动态展示Torch-MLIR将PyTorch图逐步lowering为MLIR各层dialect的编译过程,可视化算子从Torch到Linalg、TOSA、LLVM-IR的转换链路。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Torch-MLIR编译流水线 PyTorch Torch Linalg TOSA LLVM-IR dialect(n)→dialect(n+1) 逐步lowering将高层算子转换为硬件可执行代码
前端捕获
想象你是一位小说作者,拿着刚完稿的手稿走进跨国翻译社。前台没有直接开译,而是先给手稿扫描留档、登记语言方向、确认目标读者。这套“接单流程”看似繁琐,却是后面所有翻译工作的地基。Torch-MLIR 面对 PyTorch 模型同样如此:第一步不是翻译,而是捕获。
这里的“扫描留档”就是 TorchScript 捕获。PyTorch 模型本质是 Python 动态代码,里面充满 if、for 和各种 Python 对象——编译器读不懂这种“活蹦乱跳”的稿子。于是 Torch-MLIR 借道 TorchScript,把模型的前向计算固化成一张静态计算图,图上的每个节点都是明确可枚举的算子。
固化之后,torch_mlir.compile() 这部“专车”会把静态图直接送进 MLIR 世界。它有三大关键参数:模型本身、示例输入(确定输入形状)、output_type(决定翻译到哪一站下车)。在入口处,计算图被转换成 torch 方言的 MLIR 模块,模块内包含若干 func.func,每个函数体就是一层层 torch 算子。
至此,翻译社完成了接单和扫描留档。稿子内容一个字没动,只是从“作者手写体”变成了“统一版式的电子稿”。接下来,才是真正逐句翻译的时刻。
- PyTorch 模型 M_pytorch用户定义的模型对象,是一个装满权重和 forward 逻辑的容器,翻译社要处理的原稿。
- TorchScript 捕获用 torch.jit.script 或 trace 把动态的 Python 模型变成静态图,类似手稿扫描成统一的电子文档。
- 静态计算图 G_script图中节点是算子、边是张量流动,编译器才能稳定地遍历和改写。
- 图导入器 torchscript_to_torchTorch-MLIR 自带的导入 pass,把计算图逐节点翻译成 torch 方言。
- torch 方言模块 M_torch输出的 MLIR 模块,是所有后续降级工作的公共起点,相当于世界语直译稿。
Torch方言
译者拿到电子稿后,开始直译。特别注意:他翻译的是“世界语”——专有名词保留原样、句子结构照搬、修辞手法一律不动。这样做的目的是确保任何人看到直译稿,都能精确反推出原稿的每一处细节。torch 方言就是这份世界语直译稿。
torch 方言的算子集完整刻印了 PyTorch 的 ATen 算子库,名字都带着 torch.aten. 前缀,例如 torch.aten.conv2d、torch.aten.relu。一个 PyTorch 算子进来,就有一个 torch 方言算子接住,几乎一一对应。正因为映射如此直接,PyTorch 里任何一眼看不透的行为都被原样保留:inplace 修改、隐式广播、动态形状,统统写进这张稿子。
除了算子,torch 方言还引入了自己的张量类型 !torch.vtensor<[形状],dtype>。它把“运行期才知道的形状”变成了编译期可查的签名。比如 !torch.vtensor<[1,3,224,224],f32> 就清清楚楚写着这是一张 1×3×224×224 的浮点张量,为后续推导优化提供了线索。
直译稿的价值在于“保真”。它或许不够精炼,却让每个下游编辑都能放心引用:无论最终翻译成哪国语言,遇到疑问时回查这份稿子,永远能找到最原始的答案。
- 算子名torch.aten.conv2d 对应 PyTorch 的 torch.nn.Conv2d,命名也照搬 ATen 体系,方便追溯。
- 输入张量形状 [N,C,H,W] 分别表示批量大小、通道数、高、宽,是卷积输入的四个维度。
- 权重张量[F,C,k,k] 表示输出通道 F、输入通道 C、卷积核尺寸 k×k。
- 输出形状推算H' 由输入高 H、padding p、步长 s、卷积核 k 共同算出,与 PyTorch 的公式逐符号一致。
降级管线
直译稿里“断肠人在天涯”如果直愣愣交给国外读者,没人看得懂。编辑组决定意译,但不能一步到位——先统一语法、再补全缺省信息、最后才按目标语言逐句改写。Torch-MLIR 的降级管线正是这样一组按顺序执行的小型重写工序。
第一道工序是函数化(Functionalize)。PyTorch 里许多算子会原地修改张量,比如 add_ 把值直接写回输入。这类副作用让编译器不敢随意调整执行顺序。函数化 pass 把所有原地算子改写成“输入不变、返回新张量”的纯函数形式,图就变成了无副作用的函数式表达。
第二道工序是类型精化(RefineTypes)。原始 torch 方言里很多张量只有松散约束,比如“三维张量”或“未知形状”。RefineTypes 沿着算子语义把每个中间结果的形状和 dtype 精确推导出来,让隐性的信息显性化。
第三道工序是后端转换(ConvertToBackend)。这道工序根据 output_type 把 torch 算子映射到目标方言:Linalg 或 TOSA。每个 torch 算子可能有各自的改写规则,有的一个变一个,有的一个拆成多个,有的直接消失。
经过这三道工序,世界语直译稿变成了一本“通俗意译稿”。它不再保留原文的修辞痕迹,却让目标读者能顺畅理解。接下来,就看目的地在哪里了。
- 函数化(Functionalize)把 add_ 这类原地修改算子改写成 add 并返回新张量,让计算图变成纯函数。
- 无副作用每个算子只读输入、返回新输出,编译器可以放心做交换、融合、删除等优化。
- 类型精化(RefineTypes)从松散的形状约束推导出每个张量的精确形状和 dtype,相当于给所有名词补上“确切所指”。
- 后端转换(ConvertToBackend)把 torch.aten 算子逐一映射为 linalg 或 tosa 算子,是意译中最关键的一步。
Linalg衔接
有位编辑负责“学术精装版”:所有章节结构固定,逻辑链条完整,方便学者做文本比对和算法研究。Linalg 方言就是这种风格——算子不多,但每个算子都“结构化”得一丝不苟,把循环计算的三要素全部显式写出。
Linalg 的核心是 linalg.generic 这类结构化算子。它不写“我做了一次卷积”这样含糊的话,而是明确声明:迭代空间长什么样(iterator_types)、输入输出如何对齐(indexing_maps)、内部标量计算是什么(region body)。比如矩阵乘法被描述成三重循环,前两维是 parallel,最后一维是 reduction。
这种结构化表达对编译器极其友好。因为循环边界和索引关系全部摊开,多面体编译、循环融合、向量化、tiling 等优化都能批量自动实施。相比之下,一个封装严密的"conv2d"黑盒反而让优化无处下刀。
精装版图书让学者可以做严格的文本考据,Linalg 的标准化也让编译器可以做系统的循环变换。选择这个方向,等于把“性能优化”放在了首要位置。
- 矩阵乘法 MatMul语义是三重循环,C[i][j] = Σ A[i][k] × B[k][j],是深度学习里最常见的基础计算。
- 结构化算子 linalg.genericLinalg 统一模板,用声明式字段描述整类循环计算,而不是写死某个具体算子。
- 迭代类型 iterator_types前两维是并行迭代(每个输出独立),最后一维是归约迭代(需要累加),全部在循环空间显式声明。
TOSA衔接
另一位编辑负责“口袋书版”:面向地铁通勤读者,用词固定、句式短小、杜绝一切歧义。TOSA 方言正是这种风格。TOSA 全称是 Tensor Operator Set Architecture,为嵌入式设备和移动端量身定做。
TOSA 拥有一份固定的张量算子集:卷积、池化、全连接、逐元素运算、拼接、reshape 等。每个算子的语义都有唯一官方解释,不依赖运行时上下文。它不允许隐式广播——需要改变形状时必须显式写出 reshape,这样硬件在看到算子时就能确定每个维度的物理意义。
TOSA 对量化也极其友好。量化参数(零点、缩放因子)作为算子属性显式携带,定点逻辑可以一步映射到硬件指令。这让 TOSA 成为连接 ML 框架与低功耗硬件 IP 的理想中间站:芯片厂商可以直接针对 TOSA 算子做硬件综合。
口袋书虽然词汇有限,却在手机、IoT 和专用芯片的世界里畅行无阻。选择 TOSA,等于把“部署确定性”放在了首要位置。
- 语义确定每个 TOSA 算子行为有唯一官方解释,不依赖运行时上下文,硬件可按固定逻辑展开。
- 形状严明张量维度规则固定,隐式广播必须显式写 reshape,避免硬件遇到意外形状。
- 量化显式量化参数随算子属性携带,定点逻辑可直接映射到硬件指令,无需运行时推导。
后端扩展
翻译社想接新语种订单,最妙的是它不需要重译原稿和世界语直译稿,只需新配一位目标语言的意译编辑,再安排排版印刷。Torch-MLIR 的后端扩展机制正是这样设计的。
后端抽象给每个目标硬件定义一个“编辑部”。在 Python 层面,开发者用注册装饰器把后端名称和编译函数绑定起来。新增一个后端只需要:确定目标方言集合(比如 Linalg-on-Tensors、TOSA 或自定义方言)、实现 torch 算子到目标方言的降级管线、然后注册接入 compile 入口。
注册完成后,torch_mlir.compile(..., output_type="my_backend") 就能直接调用这条新产线。前端捕获和 torch 方言直译稿完全不用动——这正是“翻译社”架构的红利:所有后端共享同一份世界语底稿,任何前端改进(比如新增算子支持)自动惠及所有后端。
每一次扩展都让这家翻译社的覆盖版图更大,而成本只是“再招一位编辑”。理解了这套可插拔后端机制,你就能设计出适应未来硬件生态的编译器架构。
- 方言集选择决定从 torch-backend 降级到哪组目标方言,Linalg、TOSA 或自研方言皆可。
- 降级管线定制编写把 torch 算子逐一改写为目标方言算子的 pass 序列,是后端工作量的主体。
- 代码生成器目标方言如何变成可执行代码,通常复用 LLVM 或专用硬件工具链完成。
AI 应用场景
多硬件部署平台
阶段:模型部署与适配
解决的问题:传统方式下,要把同一个 PyTorch 模型部署到 GPU、ARM 手机、RISC-V 芯片,需要分别对接不同的推理引擎,为每个硬件手工重写算子。Torch-MLIR 让模型只需完成一次“直译”(torch 方言),再按目标平台选择 Linalg 或 TOSA 产线,后端差异被隔离在降级管线内。
具体案例:一家 AIoT 公司需要在智能摄像头(ARM CPU)、边缘服务器(x86 GPU)和自研 NPU 上同时运行同一个手势识别模型。接入 Torch-MLIR 后,算法团队只维护一份 PyTorch 源码,硬件团队各自配置 output_type 和下游编译器,一周内完成三端部署。
算子融合与性能调优
阶段:推理性能优化
解决的问题:PyTorch 模型里 Conv 后跟 BatchNorm、ReLU 是家常便饭,逐算子执行会有大量访存开销。Torch-MLIR 在 Linalg 阶段把算子摊平成结构化循环,编译器可以自动实施算子融合和循环变换,不需要手工改模型。
具体案例:某自动驾驶团队用 Torch-MLIR 编译 3D 目标检测模型,在 Linalg 层自动完成了 conv+bn+relu 的融合,端到端延迟降低 37%,且没有改动任何 PyTorch 源码。
新 NPU 芯片快速适配
阶段:后端开发与硬件适配
解决的问题:芯片公司流片前就要把主流模型跑在自己的 NPU 上,证明算力达标。从头写一个推理引擎动辄数月,而基于 Torch-MLIR 只需定制一份“目标方言 + 降级管线 + 测试闭环”,即可复用全部前端生态。
具体案例:某 RISC-V 芯片创业公司基于 Torch-MLIR 的 TOSA 后端定义了自己的 NPU 算子方言,团队三人用六周接入了 ResNet、MobileNet 等十个模型,在仿真器上完成精度验证。
Python 演示
import torchimport torch_mlir class Net(torch.nn.Module): def __init__(self): super().__init__() self.fc1 = torch.nn.Linear(784, 128) self.fc2 = torch.nn.Linear(128, 10) def forward(self, x): x = torch.flatten(x, 1) x = torch.relu(self.fc1(x)) return self.fc2(x) model = Net()model.eval()example = torch.randn(1, 1, 28, 28) for output_type in ("torch", "linalg-on-tensors", "tosa"): print("===== output_type: {} =====".format(output_type)) mlir_module = torch_mlir.compile( model, example, output_type=output_type, ) print(mlir_module)
import torch_mlir引入翻译社的 API 包,它提供 compile 入口和全部降级管线。class Net(torch.nn.Module)定义小说家的“手稿”:一个两层全连接网络,结构简单但五脏俱全。model.eval()切换到推理模式,冻结 dropout 等随机行为,让捕获的图稳定可复现。example = torch.randn(1, 1, 28, 28)提供一份“样张”,用于确定输入张量的形状信息。for output_type in (...)让同一份手稿产出三个不同“国家版本”:直译稿、学术版、口袋书版。torch_mlir.compile(model, example, output_type=...)翻译社的前台,根据 output_type 决定翻译到哪一站下车。print(mlir_module)打印 MLIR 模块,相当于翻开译稿查看每一行翻译内容。
常见误区
误区我以为 torch_mlir.compile 之后拿到的是能直接运行的 .so 或二进制文件,所以在部署 CI 里直接把它当作最终编译产物。
正解compile 返回的是 MLIR 模块(torch、linalg-on-tensors、tosa 等方言),它还需要 LLVM 等下游编译器继续翻译成目标机器码。Torch-MLIR 的职责止于“方言级翻译”,类似翻译社只出译稿,印书是印刷厂的事。
为何常见:接口名字带 compile,使用门槛又低,小白很容易联想到 gcc 这类直接产出可执行文件的编译器,忽略了中间表示的存在。
误区反正最后都要变成 linalg 或 tosa,为什么还要在中间保留一份 torch 方言?直接转换不是更省事吗?
正解torch 方言把 PyTorch 语义完整保真地固化下来,是高层次优化(如算子融合、死分支消除)和调试诊断的最佳载体。同时它是所有后端共享的公共底稿,后端只需处理“torch→自己”的降级,不用各自对接不断变化的 PyTorch 运行时。
为何常见:只看到多一层就多一次工作,忽略了跨后端复用带来的整体工程收益。
误区linalg-on-tensors 和 tosa 都是 torch 的底层方言,output_type 选哪个都一样,无非格式略有差别。
正解两者定位差异很大。Linalg 以结构化循环为核心,适合在高性能 CPU/GPU 上做融合、向量化、多面体优化;TOSA 以确定性张量算子为核心,适合嵌入式/移动端以及硬件直接推断。选错方向,后续优化和部署都会很被动。
为何常见:两者都是张量方言,命名气质相近,初学者容易忽略它们各自服务的硬件场景。
误区我看资料里写“torch 降级到 linalg”,以为是一个 pass 一次性把图里所有算子都换掉了。
正解实际是一连串几十个小 pass 逐步迭代:先函数化、再精化类型、再拆分复合算子、再逐个映射到目标方言。每一步只做很小的改写,像翻译社里不同编辑各改一遍,最后合稿。
为何常见:站在高层面看流程是“一条线”,容易把漫长接力想象成瞬间移动,忽略了工程实现的迭代细节。
误区编译完成后权重就被固化在代码里了,以后想换权重就得重新编译整个模型。
正解在 Torch-MLIR 中,模型权重通常作为全局私有常量或函数参数传递,运行时由加载器读取当前数值。图的“结构”和参数的“值”被解耦——图编译一次,权重可以动态更换。
为何常见:直觉上把编译等同于“把所有代码和数据打包”,混淆了程序静态编译与深度学习中权重-结构分离的部署设计。
面试题
Q1. torch_mlir.compile 捕获模型时,第一步先生成什么?
- A.捕获成 ONNX 图
- B.捕获成 TorchScript 图← 正确答案
- C.捕获成 LLVM IR 图
- D.捕获成 Linalg 图
答案:B。compile 先借 TorchScript 把 Python 动态模型固化为静态计算图,这样后续才能稳定地转换成 torch 方言。ONNX 是另一个生态的表示,LLVM IR 和 Linalg 都是更下游的产物。
Q2. torch 方言中的算子统一以什么前缀命名?
- A.linalg.conv 前缀
- B.tosa.conv 前缀
- C.torch.aten 前缀← 正确答案
- D.func.call 前缀
答案:C。torch.aten.* 直接沿用 PyTorch ATen 算子名,如 torch.aten.add.Tensor、torch.aten.conv2d。linalg、tosa 是降级后的底层方言,func.call 是函数调用算子,都不是 torch 方言的统一前缀。
Q3. ValueTensorType(vtensor)主要记录什么?
- A.梯度与反向传播链信息
- B.张量形状与数据类型信息← 正确答案
- C.卷积核权重分布信息
- D.算子执行字节码信息
答案:B。vtensor<[N,C,H,W],f32> 记录张量形状和 dtype,这是编译器做形状推导和算子选择的关键元数据。梯度、权重分布、字节码分别属于训练机制、数据内容和运行时产物。
Q4. 关于降级管线的说法,哪些正确?
- A.由多个小 pass 接力完成← 正确答案
- B.一个 pass 一步替换算子
- C.逐步收敛到目标方言← 正确答案
- D.不同目标对应不同管线← 正确答案
答案:ACD。lowering 由几十个小 pass 组成接力流水线,每个 pass 只做很小改写,逐步把 torch 语义收敛到 linalg/tosa 等目标;output_type 不同,选用的管线组合也不同。一步替换在工程上极难实现,B 错误。
Q5. MaximizeValueSemantics 主要做什么?
- A.原地修改算子改成函数式返回← 正确答案
- B.将函数式算子改成有副作用形式
- C.删除所有影响梯度的算子
- D.合并相邻同形状的张量
答案:A。这个 pass 让 add_、relu_ 等原地修改算子变成 add、relu 并返回新张量,使整个图变成纯函数式。纯函数式图没有副作用,编译器才能放心地做融合、交换和删除优化。
Q6. Linalg 与 TOSA 最核心的区别是?
- A.Linalg 面向循环优化,TOSA 面向嵌入式← 正确答案
- B.TOSA 面向高性能计算,Linalg 面向手机
- C.Linalg 与 TOSA 完全等价可互换
- D.TOSA 是 Linalg 的前一层表示
答案:A。Linalg 用结构化算子显式描述循环与索引映射,便于融合、向量化和多面体优化;TOSA 用确定性张量算子集服务嵌入式与硬件推断。两者各有定位,不可随意互换。
Q7. torch_mlir.compile 中 output_type 参数的作用是?
- A.控制输出张量的数据精度
- B.决定降级到的方言层级← 正确答案
- C.设置编译器的优化开关
- D.指定训练时的损失函数
答案:B。output_type 告诉翻译社“翻译到哪一站”,可选 torch、linalg-on-tensors、tosa 等。它不控制精度、不设优化等级,更与训练损失函数无关。
Q8. 为 Torch-MLIR 添加一个新后端,通常需要?
- A.定义或选择目标方言集合← 正确答案
- B.实现到目标方言的降级管线← 正确答案
- C.注册后端名称接入编译入口← 正确答案
- D.重新实现整个 PyTorch 算子库
答案:ABC。新后端需要确定目标方言、写降级管线、注册编译入口。PyTorch 算子库由 torch 方言统一接管,前端捕获也完全复用,不需要也不应该重新实现。
Q9. TOSA 为什么适合嵌入式设备?
- A.算子语义固定且张量规则严格← 正确答案
- B.运行时依赖复杂操作系统调度
- C.允许任意隐式形状广播行为
- D.需要完整 Python 解释器支持
答案:A。TOSA 每个算子的形状规则和量化参数都显式绑定,硬件可以按固定逻辑展开,无需运行时决策。隐式广播和 Python 依赖恰恰是嵌入式设备最想避免的。
Q10. 模型权重在降级后通常以什么形式存在?
- A.内联复制到每个算子调用处
- B.作为全局常量或函数参数传递← 正确答案
- C.捕获阶段直接被编译器丢弃
- D.每次前向动态重算重新获得
答案:B。权重与计算图结构解耦,通常作为模块的全局私有常量或函数参数出现在编译产物中,运行时再加载实际数值。内联到每个调用处浪费空间,丢弃或动态重算都不符合部署语义。