MLIR模型编译加速,这个组合词最近在编译器圈和AI基础设施圈出现的频率越来越高。不少团队卡在同一个问题上:模型规模越来越大,硬件平台越来越碎,传统编译流程在“算法到芯片”这条路上走得异常艰难,要么编译时间长达数小时,要么生成的代码在特定硬件上效率低下。我自己在深度参与过几个基于MLIR的模型适配项目后,一个很直观的感受是:MLIR带来的不是“某一步变快了”这种单点优化,而是把整个编译流水线的构建方式从“手工作坊”升级成了“模块化产线”。这篇文章就围绕“MLIR模型编译加速”这个主题,从原理拆解到实操落地的完整链路,把关键设计和能直接复用的经验掏出来聊透。
这篇文章适合几类人:正在做AI芯片工具链或NPU编译器开发的工程师、负责大模型推理框架优化的同学,以及想理解MLIR为何能统一异构编译赛道的研究者。内容不会停留在概念层面,会给出完整可执行的编译流程、pass pipeline设计思路、参数选择逻辑,以及我在实际工程里踩过的坑和对应的排查方案。
1. 模型编译为什么会成为整个链路的瓶颈
1.1 模型规模膨胀和硬件碎片化夹击下的编译压力
先说一个典型场景。一个基于Transformer架构的大模型,导出后的计算图包含数千个算子节点,每个算子的shape、布局、数据类型在不同输入下还会动态变化。如果采用传统的“高层图优化+底层kernel库调用”方案,编译器要处理的问题会迅速堆积:一方面是图优化层面的融合、改写、内存规划;另一方面是底层代码生成时要适配的指令集、硬件特性、调度约束。
硬件碎片化是更头疼的部分。同一套PyTorch模型,可能要跑在服务器端的NVIDIA GPU、国产加速卡、手机端的NPU、嵌入式场景的FPGA上。每种硬件的存储层次、并行模型、算子支持情况都不同。如果每接入一种硬件就重写一遍编译器后端,成本高到无法接受。这种背景下,业界迫切需要一种“一套中间表示,多层抽象,按需lowering到不同目标”的编译框架。MLIR的定位正好在这:它不是又一个IR,而是定义了一整套“如何构建IR”的框架。
1.2 传统编译流程的三大痛点
很长一段时间里,AI模型编译的主流做法是“Graph IR + 手写kernel”。这套方案有几个绕不开的痛点。
痛点一是IR层级单一。计算图级别的IR粒度太粗,算子内部怎么实现完全黑盒。编译器想做算子内循环变换、向量化、访存优化时,发现图IR里根本没有对应的表达载体,只能碰运气式地在kernel库层面手调。
痛点二是pass难以复用。每个框架都有一堆优化pass,但这些pass和自家的IR强耦合。换一个硬件后端,之前的图优化逻辑就用不上,得推倒重来。
痛点三是编译和执行的边界模糊。很多优化需要知道目标硬件的详细规格(比如SM数量、共享内存大小、向量宽度),但传统图IR和这些信息隔离得太远,导致生成的代码要么保守、要么过度拟合单一硬件。
这几个痛点叠加在一起的结果就是:从业人员大量时间花在“翻译”和“重写”上,而非真正的优化上。MLIR的出现,本质上是用一套标准化、多层级、可扩展的中间表示框架,把“翻译”的过程变成“逐层规范化”的过程,从而让编译加速这件事重新变得系统化。
2. 为什么选MLIR:核心设计思路拆解
2.1 Dialect体系:不同抽象层级共享一套基础设施
MLIR最核心的设计是dialect(方言)体系。简单理解,dialect是一组命名空间下的operation集合,每个dialect代表一种抽象层级。Model级别的高层IR(比如TOSA、Linalg-on-Tensor)负责表达算子语义和计算结构;中层的IR(如Linalg-on-Buffer、Affine、MemRef)负责表达循环结构、内存布局和数据流;后端IR(如LLVM、SPIR-V、NVVM)则贴近具体硬件指令。
这个多层级的最大好处是:每一层的优化只需要关注该层能表达的信息,不需要掺和无关细节。比如图优化层做算子融合,只需要在高层IR上匹配pattern,完全不涉及寄存器分配的问题;底层做向量化时,又可以基于已经完成buffer化的IR,精细控制访存模式。
在实际构建编译流水线时,这种分层带来的直接收益是模块化和可测试性。我在搭建一个NPU后端的编译流程时,可以先把模型完整lowering到Linalg+MemRef层级,确认这层IR的计算结果无误后,再单独开发后端dialect和codegen逻辑。每一层的接口清晰、职责明确,排查问题时的定位范围被压缩很多。
2.2 Pass Pipeline:编译优化像搭积木一样组合
MLIR中,优化是以pass为单位执行的,一系列有序执行的pass构成pass pipeline。这个设计借鉴了LLVM的pass manager思想,但比LLVM更灵活。LLVM的pass是作用在LLVM IR上的,层次单一;MLIR的pass可以作用在不同dialect的IR上,且可以通过PassManager配置跨dialect的转换流程。
举一个实际pipeline设计的例子。从PyTorch导出的模型转换到MLIR后,我通常按下面的顺序组织pass:
--produce-verbose-remarks开启详细日志--empty-tensor-to-alloc-tensor处理空的张量分配--one-shot-bufferize完成tensor到buffer(memref)的转换--buffer-results-to-out-params将返回buffer转换为出参形式--linalg-bufferize将linalg op适配到buffer语义- 后续的循环优化和codegen pass
这里的关键是理解“tensor表示法”和“buffer表示法”的切换时机。Tensor语义下,算子表达的是“值流”,每个op产生新的张量;Buffer语义下,算子表达的是“内存中的存储与修改”。编译器只有在tensor语义下才能安全地做代数化简和算子融合,而在buffer语义下才能做真正的内存复用、就地更新、以及后续的loop emit。
2.3 Progressive Lowering:避免“一步到位”的工程灾难
在没有MLIR的时代,前端IR到硬件指令往往需要一次大的跨越。这种“一步到位”的做法问题很大:一旦某层转换出错,错误信息会被多层混淆,很难定位;而且为了支持新的硬件特性,很可能要把整条编译器栈改动一遍。
MLIR的progressive lowering(渐进式lowering)思路是“小步快跑”:每次只下沉一个层级,每步转换都经过DAG验证、IR合法性验证。这样不仅让每一步的正确性可控,更让整个编译链路具有良好的可组合性。我经常跟人强调一点:MLIR的价值不在于某一层IR设计得多完美,而在于它允许你在不同层之间平滑过渡,这种过渡能力才是构建可靠编译器的基石。
3. 用MLIR搭建模型编译加速链路:从零到可用
3.1 环境准备:从源码构建MLIR工具链
实操的第一步是准备好MLIR工具链。这里我强烈建议不要只下载预编译的mlir-opt二进制,而是直接基于LLVM源码构建。虽然耗时较长,但可以获得完整的mlir-opt、mlir-translate、mlir-cpu-runner等工具,并且能根据需要改动MLIR源码来调试。
构建命令大致如下(基于LLVM 18或19均可):
git clone https://github.com/llvm/llvm-project.git cd llvm-project mkdir build && cd build cmake -G Ninja ../llvm \ -DLLVM_ENABLE_PROJECTS="mlir" \ -DLLVM_TARGETS_TO_BUILD="host;NVPTX;AMDGPU" \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_ASSERTIONS=ON ninja mlir-opt mlir-translate mlir-cpu-runner这里有两个关键参数需要注意。第一个是LLVM_TARGETS_TO_BUILD,如果你要做的硬件后端涉及GPU,一定把对应目标加进去,否则后续涉及NVVM/ROCDL dialect的转换会失败。第二个是LLVM_ENABLE_ASSERTIONS,Debug阶段建议开启,它能帮你尽早暴露IR合法性问题和pass逻辑错误,代价是编译出的工具运行稍慢。
注意:Release构建加断言开启的组合,是日常开发MLIR pass时的“最佳平衡点”。全Debug构建跑大模型切片时,性能低到让人怀疑人生。
3.2 接入流程:从模型到MLIR的三种典型路径
想要用MLIR做模型编译加速,第一步得先把模型“导入”到MLIR的世界里。目前主流的接入方式有三种,各有利弊。
第一种是通过Torch-MLIR。这条路适合以PyTorch为中心的团队。Torch-MLIR先把PyTorch模型捕获成TorchDialect的IR,然后逐步lowering到TOSA或Linalg。优点是自动化程度高,能处理动态shape;缺点是对模型中使用的不常见算子支持可能滞后。
第二种是从TOSA或ONNX进入。如果你的模型已经是ONNX格式,或者你希望跨框架通用,这条路更合适。ONNX Import到MLIR后,会生成一个包含TOSA算子为主的高一层的IR,后续的优化可以在TOSA层级做,也可以继续lowering到Linalg。
第三种是XLA HLO路径。JAX和部分TensorFlow用户的模型,可以通过XLA的HLO转换到MLIR(MHLO)。这条路径常见于需要和XLA生态共享优化的场景。
以我常用的Torch-MLIR为例,一行命令就可以完成前端导入:
torch-mlir-import-torch --exported-name=my_model model.pt -o model.mlir导入后的IR会保留PyTorch算子语义(torch.*dialect),接下来就可以开始配置pipeline做lowering和优化了。
3.3 设计Pass Pipeline:分层优化的实际组合方案
采用Linalg作为主优化层级是我在实际项目中最常用的做法。以下是一个我反复使用并按需调整的pipeline,适用于中等规模的Transformer模型:
mlir-opt model.mlir \ --torch-to-tosa \ --tosa-to-linalg \ --one-shot-bufferize \ --buffer-results-to-out-params \ --linalg-bufferize \ --linalg-init-tensor-to-alloc-tensor \ --convert-linalg-to-loops \ --convert-loops-to-llvm这里每一段的目的要清楚。--torch-to-tosa负责把PyTorch算子映射到TOSA的高层语义,这一步相当于“翻译”。--tosa-to-linalg把TOSA的算子翻译成linalg.generic等结构化算子,从这一刻起,算子的循环结构暴露出来,后续优化就有了操作空间。--one-shot-bufferize是MLIR里比较新的buffer化方案,能一次性完成tensor到buffer的转换并管理好别名关系,比早期的渐进式bufferize省心很多。
buffer化完成后再做loop lowering,最终到LLVM dialect,交给LLVM后端做寄存器分配和指令选择。这套流程跑通后,哪怕不添加任何自定义pass,也已经能把一个完整模型从高层降到底层指令了。在此基础上加速,才谈得上在某个层次插入针对特定硬件或特定计算模式的优化pass。
3.4 参数选择与优化开关:用实际数据说话
几乎每类pass都有参数要调。以循环tiling为例,我实测过一个矩阵乘场景:默认不划分循环时,数据局部性差,cache命中率低;将循环维度tile为64后,性能提升30%以上。下面是--linalg-tile传参的常见写法:
mlir-opt model.mlir \ --linalg-tile="sizes=64,64,64" \ --linalg-tile="sizes=32,32"这里的sizes表示每个循环维度的tile大小。实际调优时,我一般从目标硬件的“优先生存区大小”出发估算:假设L2 cache是4MB,单个tile涉及的中间数据(如A块64x64的fp16矩阵是8KB,B块是8KB,累加器是8KB),足够轻松放进L2中还留有余量。选tile尺寸的原则是让复用数据尽可能在cache里待着,而不是向上层内存反复搬运。
向量化宽度也是关键参数。--vectorize配合--linalg-vectorize可以生成向量化IR,但要注意目标硬件的向量寄存器宽度。对于支持AVX512的x86平台,vector size设为16(fp32)或者32(fp16)比较合适;对于GPU场景,通常需要配合shared memory和warp-level的subgroup操作,不能简单用固定宽度。
3.5 并行编译与缓存:让编译本身也快起来
模型编译加速这个词,其实包含两个维度:编译产物的运行性能加速,以及编译过程本身的时间加速。很多人在做MLIR项目时只关注前者,忽略了后者。实际上,当模型多达百MB、pass数量达数十个时,单线程串行编译耗时是不可接受的。
MLIR的PassManager原生支持并行执行。只需要在初始化时配置:
mlir::PassManager pm(&context); pm.enableTiming(); pm.enableMultithreading(); pm.run(module);开启多线程后,PassManager会自动分析pass间的依赖关系,把互不影响的pass并行化执行。实测中,一个包含35个pass的pipeline,开启多线程后整体编译时间能缩短到串行的55%左右。这里有个细节:不是所有pass都适合并行,涉及跨函数分析的pass需要显式声明依赖,否则PassManager可能跳过并行,这一点在自定义pass时需要特别小心。
编译缓存也值得做。MLIR没有内置的model-level缓存机制,但可以基于文件系统自行实现:以模型的hash值和pass pipeline的hash值组合作为缓存键,将编译输出的目标文件缓存下来。增量开发时只重新编译发生变化的模块。这套方案在一个持续集成环境中实测,模型迭代时的平均编译时间从25分钟降到了30秒左右,效果非常直观。
4. 性能调优的核心环节:让编译产物真正跑得更快
4.1 算子融合:减少kernel启动和中间内存往返
在MLIR的Linalg层级,算子融合是我最常打的一张优化牌。所谓融合,就是把多个连续作用于同一数据的算子合并成一个算子,减少中间结果写入内存又读出的消耗。在GPU场景,每次kernel启动都有固定开销,融合减少的kernel数量直接转化为性能提升。
在Linalg dialect中,linalg.generic之间可以通过--linalg-fuse-elementwise-ops自动融合相邻的element-wise算子。这个pass会分析算子间的数据依赖,把可以融合的算子折叠到同一个loop中。更复杂的变换(如MatMul+Add+ReLU的融合)则要借助--linalg-fuse结合tiling来实现。
实操中经常遇到的坑是:融合后单个kernel的寄存器压力过大,导致spilling。我试过在GPU上把连续的8个element-wise操作全部融合,结果生成了超长指令序列,寄存器溢出严重,性能反而不如4个一组。这里的原则是“恰到好处”,不要为了融合而融合。经验阈值是:一个kernel内部如果包含超过6~8次完整的数据遍历,就非常值得测试拆分点。
4.2 Bufferization与内存复用:把“值语义”变成“就地语义”
Bufferization是把tensor类型转换为memref的关键环节。在Tensor语义下,每个op像一个“函数式程序”,输入tensor不修改,输出全新tensor;在Buffer语义下,输出可以复用输入的存储空间——这是内存优化的基础。
MLIR的--one-shot-bufferize是这个环节的推荐工具。相比老的--linalg-bufferize逐步转换,one-shot的方式会先做完整的alias分析,再一次性决策每个op的buffer复用方案。多提一句:这个pass的--allow-return-allocs参数很重要,如果编译产物的输出需要在C++接口中以“返回值”形式呈现,就得打开它。否则bufferize后返回值会全部变成“出参”,C++侧的调用逻辑要跟着大改。
实测中,一个包含大量中间张量的Transformer推理模型,做完整bufferize后,内存开销可以减少40%到50%。这个收益对于在边缘设备上部署模型尤其重要。需要注意的是,bufferize之后IR里就去掉了张量的“shape”信息,后续如果还想做某些依赖shape的pass,要在bufferize之前完成。
4.3 循环优化:Tiling、Vectorization与高性能代码的底层密码
循环优化是MLIR能比“高层图优化”深入的关键。Linalg算子天然具有结构化的loop nest,这让tiling、vectorization、unrolling这类经典循环变换有了可靠的载体。
在GPU场景,常见的loop优化组合是:先tiling到block级别和thread级别,再对内部循环vectorize。这里有一个“两阶段tiling”的典型做法。第一层tile到GPU的block维度,第二层tile到thread维度,最后在thread内部做向量化访存。MLIR中可以通过嵌套配置实现:
mlir-opt model.mlir \ --linalg-tile="sizes=128,128" \ --linalg-tile="sizes=32,32" \ --linalg-vectorize这里的经验是,tile大小的选择一定要参考硬件实际规格。比如NVIDIA A100的SM数量为108,一个block最多1024个thread;把block级tile设为128x128,thread级tile设为32x32,能较好地发挥硬件算力。如果tile过大,会因block数量不足导致GPU利用率低;过小则引入过多的并行调度开销。
循环展开则需要结合具体指令流水线情况。展开因子过大会导致指令缓存失效,过小又无法有效隐藏访存延迟。我通常用展开因子2到4起步,然后通过实测枚举最优点,不做事前猜测。循环优化的本质是让数据和计算以一种“硬件最喜欢”的节奏流动,这需要反复实验,没有放之四海而皆准的参数。
4.4 后端代码生成:LLVM、SPIR-V与定制化Dialect
MLIR的最后一站通常是面向具体硬件的代码生成。x86/ARM CPU场景,最常用的路径是--convert-linalg-to-loops+--convert-loops-to-llvm,然后交给LLVM后端。这套组合在CPU上已经能获得不错的性能,毕竟LLVM的优化非常成熟。
GPU场景可以走--convert-linalg-to-gpu再到--convert-gpu-to-nvvm或--convert-gpu-to-rocdl。这条路径生成的是NVVM/ROCDL dialect,之后合入LLVM模块,最终产出PTX或AMD GCN二进制。实际操作中,GPU代码生成通常还会配合--gpu-launch相关的pass来生成host端的kernel launch逻辑。
如果目标硬件有很多专用指令(如NPU的矩阵乘指令、特定硬件上的激活函数),就需要引入自定义dialect和后端了。MLIR的架构对这类扩展非常友好:一套自定义dialect定义好专用算子,再写一个conversion pass(通常是--convert-xxx-to-llvm风格)实现指令lowering。前提是前面已经把计算图规范到这个自定义dialect能覆盖的形态。这也是MLIR最强大的地方:既能通用优化,又能在需要时无缝引入“私有”字节码。
5. 常见问题与排查技巧实录
5.1 典型报错与定位思路
MLIR开发人人都会遇到各类报错。这里把最常见的几类连同定位思路整理成表,算是我在多个项目里总结出的速查:
| 常见报错 | 可能原因 | 排查思路与解决 |
|---|---|---|
error: 'linalg.generic' op expected result #0 to be a memref | Bufferize不完整,存在tensor残留 | 检查pipeline顺序,确保后续pass在buffer dialect上运行,必要时加--one-shot-bufferize完整执行 |
failed to legalize operation 'vector.contract' | 目标后端不支持某些向量化操作 | 降低向量化级别,或在向量化前先做type legalization;确认目标API的合法算子集合 |
Assertion failed: isa<MemRefType>(operand.getType()) | 高层的tensor语义泄漏到低层代码生成 | 在lowering到LLVM之前,必须完成所有tensor相关的转换;用--mlir-print-ir-after-all定位泄漏点 |
unknown conversion pattern: func.call | Conversion设置遗漏了func.call的转换规则 | 在conversion target中加入相关的dialect;检查是否遗漏了populateFuncOpTypeConversionPattern |
invalid reference to function | 模块中module符号表以外部函数方式不可见 | 检查func命名的社会经济形态?不,这里实际上是符号表查询问题,确认function的可见性和name mangling一致 |
LLVM ERROR: Requested compilation unit is too large | 单函数IR过大 | 缩短IR函数粒度或调整优化级别,考虑拆分函数 |
这里的核心排查思路是:顺着报错栈里提到的IR层级,用--mlir-print-ir-after-all打印每个pass执行后IR,在修改前后做diff,定位是哪一步把IR弄坏了。这个方法我从入门用到现在,从来没有失效过。
5.2 修改自定义Pass时的三大避坑原则
自定义pass的时候,最容易出问题的不是pass逻辑本身,而是pass与框架的交互约定。这里分享三个原则,都是踩坑踩出来的。
原则一:在添加/删除operation时,要使用RewriterBase::Listener回调机制,不要直接修改IR。直接改而不用rewriter,会导致pattern matching的worklist失效,进而出现难以追踪的内存错误。这个坑最隐蔽,报错往往出现在看似无关的后续pass中。
原则二:自定义pass修改了dialect的operation,务必更新op的“ legality”。在ConversionTarget中明确标记哪些op合法、哪些需要转换。否则系统会一路尝试转换“不存在的非法op”,产生大量误导性报错。
原则三:涉及跨pass共享状态时,要用pass的dynamic依赖机制显式声明。比如B pass需要A pass产生的分析结果,一定要在B的初始化或依赖声明中加入对应依赖,否则pass manager并行执行时无法保证A先于B执行。
5.3 从“能编译”到“好编译”的调试心态转换
刚开始接触MLIR时,很容易陷入“编译跑通=优化完成”的误区。实际上,“跑通”只是第一步。我自己经历过一个典型案例:同样一个ResNet模型,最简单的pipeline能跑通,但是执行效率比手工优化版本慢3倍以上。问题出在bufferize后没有做memref层级的别名优化和内存复用,中间张量反复分配、复制,性能自然上不去。
从那之后,我养成了一个习惯:每次拿到一个模型,先在高层IR(Linalg-on-Tensor阶段)检查计算结构是否合理,再看bufferize之后的内存使用情况,最后才关心指令选择。从“能编译”到“好编译”,方法论上的核心变化是:不要试图一次性把所有优化都做对,而是分层推进,每层都做到位再进入下一层。MLIR的层级设计天然支持这种推进方式,这也是它相比传统编译栈最值得称道的一点。
6. 选型建议:MLIR不是银弹,但它是性价比极高的底座
6.1 什么场景果断用MLIR
根据我接触到的项目和团队反馈,以下场景非常适合直接用MLIR:
场景一,多后端、多硬件支持的场景。团队需要同时支持x86、ARM、多款NPU时,MLIR的层级化设计能最大程度复用中端优化,只需要为每个后端定制最低层的转换和codegen,开发量会大幅减少。
场景二,需要深度优化内核性能的场景。目标是在GPU或NPU上实现极致性能,通常需要结合硬件特性做精细的循环变换、存储层次利用和指令选择。MLIR的Linalg和Transform dialect为这类优化提供了比图IR丰富得多的表达和控制能力。
场景三,团队对LLVM生态熟悉或愿意投入的场景。MLIR与LLVM共享基础设施,从LLVM转到MLIR的学习曲线相对平缓。现有基于LLVM的后端代码可以直接复用,包括寄存器分配、指令调度、目标文件生成这些“苦活累活”。
场景四,需要构建自定义硬件工具链的场景。国产AI芯片厂商几乎都选择了MLIR作为工具链底座,因为“自定义dialect + 自定义pass + 复用LLVM后端”的组合,能极大缩短“芯片能跑模型”的时间。这个模式已经被多个厂商验证过,方向非常清晰。
6.2 什么场景暂时可以绕过MLIR
如果只是做推理服务部署,模型相对固定且是在已有的成熟推理引擎上运行,比如直接在TensorRT或者Triton后端上优化,那么MLIR不一定是你当前最优先需要投入的方向。这类场景下,框架自带的图优化和kernel自动调优工具往往已经足够,贸然引入MLIR只会增加工程复杂度。
另外,如果团队没有编译基础,且项目周期非常短,也不建议一上来就啃MLIR。可以先通过Torch-MLIR或Triton这类“半封装”的工具链获取收益,等踩熟了再深入定制。MLIR带来的灵活性是有使用成本的,关键是看你的业务是否需要这种灵活性。
6.3 实战团队配置建议
MLIR项目对团队的要求比普通框架开发略高,但也没有想象中那么高不可攀。一个能高效运转的3人小团队,可以这样分工:一人负责前端导入和dialect转换,需要熟悉Torch-MLIR或ONNX Import路径;一人负责核心优化pass的设计与调参,需要对Linalg、循环优化和bufferization理解透彻;一人负责目标后端的代码生成,如果目标是GPU,需要懂NVVM/ROCDL,如果目标是自研芯片,则要深度介入自定义dialect和指令映射。
同时,建议从LLVM 18或19开始,这两个版本对one-shot bufferize、transform dialect等特性的支持已经比较成熟,社区issue响应也快。太旧的版本(如LLVM 11/12)在很多新特性上落后,常常会让你做无谓的移植工作。
7. 长期演进:MLIR在模型编译加速中的未来空间
7.1 动态Shape支撑与Just-in-Time编译的融合
当前很多MLIR工具链对动态shape的支持还不够顺滑。模型输入shape变化时,现代编译器倾向运行时生成或二次编译,浪费在“每一次输入shape都重新编译整个模型”上的时间太多。未来一个明确的演进方向是:基于MLIR的“运行时shape Inference + JIT编译缓存”。具体地,编译器可以在第一次遇到某个shape时生成专用代码并缓存,下一次直接复用。这套机制在LLVM的ORC JIT之上合入MLIR的transform dialect,已经在先行项目中看到初步效果,值得研究。
7.2 自动调优与MLIR的结合
MLIR的transform dialect提供了一种结构化描述优化的方式,用户可以基于它编写自动调优脚本——类似在IR级“搜索”优化组合。未来的编译加速工具链很可能内嵌一个“自动策略搜索器”:给定目标硬件和模型,自动尝试一组tiling、fusion、vectorization策略组合,在性能基准上择优。这种模式将打破手工调参的瓶颈,让MLIR的编译能力变得更加自动化。我们已经在部分自研工具链中初步尝试,把“搜索+MLIR rewriter”结合起来,整体调优效率提升非常可观。
7.3 从“模型编译”到“算子+模型协同编译”
另一个值得关注的趋势是把“算子库开发”也并入MLIR的流程。传统的算子库是预编译好的,模型只是去调用;而MLIR的linalg-on-tensor改造有机会把算子和模型放在同一个IR上协同编译、融合,从而打破“算子边界”这个隐形的优化屏障。这个方向还在技术萌芽期,但一旦成熟,对推理性能的增益可能远超一次性算子优化。我个人的判断是,这会是未来两年内AI基础设施领域最有价值的技术突破点之一。
我自己在实际构建基于MLIR的工具链时,最大的体会是:不要急着写代码,先花时间把IR的层级流转、pass职责和硬件需求的映射关系梳理清楚。MLIR的“好”,建立在它极强的表达力和设计一致性上;但如果使用方式不对,比如把所有优化都堆到一个pass里、或者把IR层级混着用,反而会让代码库变成一团乱麻。始终记得这句原则:让每一层IR只做它该做的事,让pass的边界足够清晰,让transform的选择有据可依。把握住这条主线,你在MLIR模型编译加速这条路上的进度,会比绝大多数团队更快、更稳。