我手上有一个MLIR项目,项目使用的LLVM版本为20.1.8,之前一直是在x86-64的Debian Linux学校服务器上编程,调试并运行。这两天突发奇想,想把项目的转到新入手的Mac Mini M4上,于是就撞上了这个奇怪的问题🤔

Mac环境配置

通过HomeBrew安装llvm@20

brew install llvm@20

为此顺带将项目参数配置改为CMake的Preset

{
"version": 3,
"configurePresets": [
{
"name": "macos-llvm20-debug",
"displayName": "MacOS LLVM 20 Debug Config",
"binaryDir": "${sourceDir}/build",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "/opt/homebrew/opt/llvm@20/bin/clang",
"CMAKE_CXX_COMPILER": "/opt/homebrew/opt/llvm@20/bin/clang++",
"LLVM_DIR": "/opt/homebrew/opt/llvm@20/lib/cmake/llvm",
"MLIR_DIR": "/opt/homebrew/opt/llvm@20/lib/cmake/mlir",
"CMAKE_OSX_SYSROOT": "macosx",
"CMAKE_OSX_DEPLOYMENT_TARGET": "26.0"
}
},
{
"name": "macos-llvm20-release",
"displayName": "MacOS LLVM 20 Release Config",
"binaryDir": "${sourceDir}/build",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "/opt/homebrew/opt/llvm@20/bin/clang",
"CMAKE_CXX_COMPILER": "/opt/homebrew/opt/llvm@20/bin/clang++",
"LLVM_DIR": "/opt/homebrew/opt/llvm@20/lib/cmake/llvm",
"MLIR_DIR": "/opt/homebrew/opt/llvm@20/lib/cmake/mlir",
"CMAKE_OSX_SYSROOT": "macosx",
"CMAKE_OSX_DEPLOYMENT_TARGET": "26.0"
}
},
{
"name": "debian-llvm20-debug",
"displayName": "Debian LLVM 20 Debug Config",
"binaryDir": "${sourceDir}/build",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "/usr/lib/llvm-20/bin/clang",
"CMAKE_CXX_COMPILER": "/usr/lib/llvm-20/bin/clang++",
"LLVM_DIR": "/usr/lib/llvm-20/lib/cmake/llvm",
"MLIR_DIR": "/usr/lib/llvm-20/lib/cmake/mlir"
}
},
{
"name": "debian-llvm20-release",
"displayName": "Debian LLVM 20 Release Config",
"binaryDir": "${sourceDir}/build",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_C_COMPILER": "/usr/lib/llvm-20/bin/clang",
"CMAKE_CXX_COMPILER": "/usr/lib/llvm-20/bin/clang++",
"LLVM_DIR": "/usr/lib/llvm-20/lib/cmake/llvm",
"MLIR_DIR": "/usr/lib/llvm-20/lib/cmake/mlir"
}
}
]
}

问题表现

MLIR输出被降级为Generic Form——以这种形式表现的MLIR多半运行会出现问题

"builtin.module"() ({
"func.func"() <{function_type = (index) -> !operate.plainaggregatecontext, sym_name = "pipeline_0"}> ({
^bb0(%arg2: index):
%3 = "operate.plainAggregateInit"() <{agg_value_columns = [[1700 : i32, 0 : i32, 0 : i32]]}> : () -> !operate.plainaggregatecontext
%4 = "operate.scanInit"() <{batch_size = 2048 : i64, cols = ["l_orderkey", "l_partkey", "l_suppkey", "l_linenumber", "l_quantity", "l_extendedprice", "l_discount", "l_tax", "l_returnflag", "l_linestatus", "l_shipdate", "l_commitdate", "l_receiptdate", "l_shipinstruct", "l_shipmode", "l_comment"], table = "lineitem"}> : () -> !operate.scancontext
"scf.while"() ({
%7 = "operate.check_hasMoreBatch"(%4) : (!operate.scancontext) -> i1
"scf.condition"(%7) : (i1) -> ()
}, {
%5 = "operate.scanNext"(%4) : (!operate.scancontext) -> !operate.batch
%6 = "operate.filter"(%5) <{predicate = [...]}> : (!operate.batch) -> !operate.batch
"operate.plainAggregateSource"(%6, %3) <{...}> : (!operate.batch, !operate.plainaggregatecontext) -> ()
"scf.yield"() : () -> ()
}) : () -> ()
"operate.scanDestroy"(%4) : (!operate.scancontext) -> ()
"func.return"(%3) : (!operate.plainaggregatecontext) -> ()
}) : () -> ()
"func.func"() <{function_type = (!operate.plainaggregatecontext) -> !operate.batch, sym_name = "pipeline_1"}> ({
^bb0(%arg1: !operate.plainaggregatecontext):
%2 = "operate.plainAggregateSink"(%arg1) <{agg_value_works = [[1700 : i32, 0 : i32, 0 : i32]]}> : (!operate.plainaggregatecontext) -> !operate.batch
"func.return"(%2) : (!operate.batch) -> ()
}) : () -> ()
}) : () -> ()

正常显示的MLIR应该是下面这样

module {
func.func @pipeline_0(%arg0: index) -> !operate.plainaggregatecontext {
%0 = operate.plainAggregateInit([[1700 : i32, 0 : i32, 0 : i32]]) -> !operate.plainaggregatecontext
%1 = operate.scanInit {batch_size = 2048 : i64, cols = ["l_orderkey", "l_partkey", "l_suppkey", "l_linenumber", "l_quantity", "l_extendedprice", "l_discount", "l_tax", "l_returnflag", "l_linestatus", "l_shipdate", "l_commitdate", "l_receiptdate", "l_shipinstruct", "l_shipmode", "l_comment"], table = "lineitem"} : () -> !operate.scancontext
scf.while : () -> () {
%2 = operate.check_hasMoreBatch(%1) : (!operate.scancontext) -> i1
scf.condition(%2)
} do {
%2 = operate.scanNext(%1) : (!operate.scancontext) -> !operate.batch
%3 = operate.filter %2 {predicate = [...]} : (!operate.batch) -> !operate.batch
operate.plainAggregateSource(%3, %0) {agg_detail_works = [...], agg_value_columns = [[1700 : i32, 0 : i32, 0 : i32]], agg_work_columns = [[2114 : i32]]} : (!operate.batch, !operate.plainaggregatecontext) -> ()
scf.yield
}
operate.scanDestroy(%1) : (!operate.scancontext) -> ()
return %0 : !operate.plainaggregatecontext
}
func.func @pipeline_1(%arg0: !operate.plainaggregatecontext) -> !operate.batch {
%0 = operate.plainAggregateSink(%arg0, [[1700 : i32, 0 : i32, 0 : i32]]) -> !operate.batch
return %0 : !operate.batch
}
}

如果显示错误但能运行也还好,可下一步MLIR转LLVM IR立马就报SegmentFault,这不要太恼火🤨

问题解决流程

这种没思路的问题,当然是交给AI处理🤓

先用Github Copilot的Gemini Pro尝试,但由于今天是月底。Github Copilot的额度快用光了,在额度仅剩1%的时候把SegmentFault消除了——但代价是CMakeList.txt一片狼藉,对此Gemini Pro的解释如下

  1. 脑裂问题 (Split-Brain):
    • 项目同时链接了静态库 (libMLIROperate.a) 和动态库 (MLIRExecutionEngine -> libMLIR.dylib)。
    • 这导致内存中存在两套 MLIR 注册表。Dialect 在其中一个注册表中加载,但打印时使用的是另一个注册表,导致无法识别操作符,从而回退到 Generic Form。
    • 修复: 从 CMakeLists.txt中移除了 MLIRExecutionEngine,强制全静态链接。
  2. 符号剥离 (Symbol Stripping):
    • MacOS 的链接器 (ld64) 默认会剥离静态库中未被显式调用的符号。由于 Dialect 是通过全局构造函数或反射机制注册的,链接器误以为它是无用的代码而将其丢弃。
    • 修复: 在 CMake 中添加了 -Wl,-force_load,$<TARGET_FILE:MLIROperate>,强制链接器包含 MLIROperate 库的所有符号。
  3. 构建配置:
    • 修正了 add_llvm_executable 的使用,添加了 PARTIAL_SOURCES_INTENDED
    • 启用了 -fexceptions 以支持代码

这个回答中,2是错误的,去掉这部分内容的修改不会影响运行。3对了一半,把我原本写成add_executable的代码改成了add_llvm_executable ,这并不是我想看到的,实际撤销后也不影响运行

这里插个眼,似乎使用add_llvm_executable并配上 PARTIAL_SOURCES_INTENDED可以加速编译,具体情况后续可以跟进

问题此刻就来到了1上,使用MLIRExecutionEngine的主要工作就是将MLIR转LLVM IR并进行O3优化,于情于理都不应该影响MLIR的输出,在反复调试后发现问题还真出在这里。

最终的解决方案是:引入LLVM IR的Pass,就地对LLVM Module优化并输出

原本的代码:

#include "mlir/ExecutionEngine/ExecutionEngine.h"
#include "mlir/ExecutionEngine/OptUtils.h"
mlir::ExecutionEngine::setupTargetTripleAndDataLayout(llvmModule.get(),
tmOrError.get().get());
auto optPipeline =
mlir::makeOptimizingTransformer(3, 0, tmOrError.get().get());
if (auto err = optPipeline(llvmModule.get())) {
llvm::errs() << "Failed to optimize LLVM IR " << err << "\n";
return -1;
}

现在的代码(optimizeModule是手写的LLVM Pass集合函数):

#include "llvm/Analysis/CGSCCPassManager.h"
#include "llvm/Analysis/LoopAnalysisManager.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/StandardInstrumentations.h"
llvmModule->setDataLayout(tmOrError.get()->createDataLayout());
llvmModule->setTargetTriple(tmOrError.get()->getTargetTriple().str());
optimizeModule(*llvmModule, *tmOrError.get());

问题分析

由于我的Github Copilot的额度已用完,分析工作就交给了Kimi CLI。正好前一天搓了一版MLIR的MCP的MLIR文档RAG派上了用场(我先测试一段时间,如果真的好用的话,也许以后有机会可以讲讲)

结合MLIR文档和调试,解释mlir::ExecutionEngine是如何造成这个问题的?

用户要求深入解释 mlir::ExecutionEngine 是如何具体造成 MLA 格式变化的问题的。这个问题需要:

  1. 查看 MLIR 相关文档,了解 ExecutionEngine 如何影响 MLIR 上下文和序列化
  2. 分析代码中的具体变化点
  3. 结合调试信息,理解 ExecutionEngine 如何改变 MLIR 的默认行为
  4. 解释为什么这种改变会导致段错误

让我逐步深入分析。 • 让我深入研究这个问题,查看MLIR代码和调试信息来理解mlir::ExecutionEngine如何影响MLIR生成。 • Used search_mlir_knowledge ({“query”: “ExecutionEngin… generic vs pretty form”}) • Used search_mlir_knowledge ({“query”: “MLIRContext re…nEngine dialect loading”}) • Used search_mlir_knowledge ({“query”: “MLIR builtin m…t generic assembly form”})

……

根据从MLIR文档中学到的知识:

  1. MLIR有两种格式:Pretty(简洁)和 Generic(通用字典格式)
  2. Generic格式在操作未注册或方言未加载时使用
  3. Dialect必须明确加载到MLIRContext中

可以看到Kimi CLI很丝滑的进行了MCP接口的调用,而输出质量,从我使用的感觉上来说,要比没有MCP的输出要强很多——在这之前这种工作,我肯定是要用Claude Opus 4.5的,但现在有了MCP进行RAG后,输出看起来让人放心许多。

最终结论概括如下

• 静态链接副作用:MLIRExecutionEngine引入的依赖链 • 静态初始化器:在main()之前执行,干扰MLIR全局注册表 • 方言注册干扰:破坏自定义printer/parser的查找 • 回退机制:MLIR无法找到自定义printer时回退到通用格式

MLIRExecutionEngine被链接的时候,其依赖链如下

MLIRExecutionEngine
MLIRTargetLLVMIRExport
MLIRLLVMToLLVMIRTranslation
LLVM IR Libraries (with static initializers)

这种静态链接会造成Side Effect破坏MLIR注册的Context,导致自定的MLIR无法被识别,进而输出generic form的MLIR致使SegmentFault

结语

虽然这个解释并没有说明在x86-64 Debian Linux上为何能顺利运行,但尚且还在可接受范围之内——如果细究这个问题,还会牵扯到链接器(linker)实现,符号命名空间,ABI,LTO(链接时间优化),函数执行顺序等一系列有关编译器的问题——而这个问题多半是函数执行顺序变化导致的

所以进一步,还能得出以下结论😂:

为何Linux上工作正常

  1. 静态初始化器执行顺序稳定
  2. 保守的链接时优化
  3. 符号可见性控制更好
  4. 初始化器不会被去重

风险 即使Linux上现在工作正常,未来可能因以下原因出问题:

  • 工具链升级

  • 静态链接

  • 不同发行版的差异

  • 开启LTO优化

建议:即使Linux上没问题,也应该移除MLIRExecutionEngine,因为:

  1. 这是正确的架构分离
  2. 避免未来潜在问题
  3. 保持跨平台一致性
  4. 减少依赖

当然,能看到之前的工作能顺利搬迁到MacOS上,以及这两天写的MCP工具确实在实战中证明有用,这两件事还是值得庆祝的🎉