Mojo 编译器 Post-Parser 调试完全指南:用 kgen 系列工具剖析 MLIR 与 LLVM IR
2026/9/10 16:01:35 网站建设 项目流程

Mojo 编译器 Post-Parser 调试完全指南:用 kgen 系列工具剖析 MLIR 与 LLVM IR

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

导读

本指南以 Mojo 编译器仓库中的 PostParserDebugging.md 为骨架,系统讲解 Mojo 编译流水线在 Elaboration 之后的调试方法。你将掌握kgenkgen-optkgen-llvm-optllvm-module-split等内置工具的使用方式与各自定位,学会如何定位"肇事 Pass"、转储中间表示(MLIR/LLVM IR/bitcode/汇编)、借助KGEN_OPTIONS环境变量注入 Pass 级调优参数,以及如何用lldb/gdb调试编译器本身。文章还深入覆盖 Apple GPU 后端(AIR 文件)的专项调试流程,包括 invalid bitcode、错误输出与Failed to create compute pipeline state三类典型问题的排查方法。

为什么需要"Post-Parser 调试"

Mojo 编译器的前端把 Mojo 源码解析、语义检查并 Elaborate(展开)成 MLIR 之后,剩下的大量工作——从 MLIR 到 LLVM IR 的降级(lowering)、自定义 LLVM 优化管线、后端汇编/对象生成——都发生在解析器(Parser)之后。当生成的 MLIR 或 LLVM IR 不正确时,问题往往出在某个具体 Pass 上,此时就需要一套面向中间表示的调试工具链。

从仓库源码看,这套工具集中位于 Mojo/tools 目录下,构建目标由各子目录的BUILD.bazel定义(如 Mojo/tools/kgen/BUILD.bazel、Mojo/tools/kgen-opt/BUILD.bazel 等)。它们共同构成了 Mojo 编译器"解析之后"的观察窗口。

调试工具全家福

工具用途源码位置
kgen把 Mojo 程序编译到由命令行选项控制的某个阶段并输出结果Mojo/tools/kgen/kgen.cpp
kgen-opt类似mlir-opt,可对给定 MLIR 单独运行某个 PassMojo/tools/kgen-opt/kgen-opt.cpp
kgen-llvm-opt测试 Mojo 自定义 LLVM 优化管线(类似optMojo/tools/kgen-llvm-opt/kgen-llvm-opt.cpp
llvm-module-split测试把 LLVM IR 拆分到不同模块的 splitterMojo/tools/llvm-module-split
kgen-reduce归约(reduce)MLIR 以便调试(仓库中标注 NOT TESTED,较少使用)Mojo/tools/kgen-reduce

其中kgenkgen-opt是日常调试的主力,下面分别展开。

kgen:按阶段截停编译

kgen的核心逻辑在 Mojo/tools/kgen/kgen.cpp 的runToolPipeline函数中,它按命令行指定的Command决定在哪一步停下来并输出中间产物。文档给出的选项如下:

  • -elaborate:在 Elaboration(宿主侧)结束后立即停止,打印结果 MLIR;
  • -emit=llvm:在 MLIR 降级为 LLVM IR(宿主侧)后停止,打印 LLVM IR;
  • -emit=llvm-bitcode:同上,但输出 LLVM bitcode;
  • -emit=llvm-opt:跑完 Mojo 自定义 LLVM 优化管线后停止,打印结果 LLVM IR;
  • -emit=llvm-opt-bitcode:同上,输出 bitcode;
  • -emit=asm:跑完自定义优化管线并完成后端汇编,输出汇编;
  • -emit=asm-verbose:同asm,但内联更多信息;
  • -emit=object:输出二进制目标文件;
  • 其他选项见--help,调试中较少使用。

从源码看,kgenmain中注册了registerMLIRContextCLOptionsregisterAsmPrinterCLOptionsregisterDefaultTimingManagerCLOptionsregisterPassManagerCLOptions以及KGEN::KGENPassCLOptions::registerOptions()(见 Mojo/tools/kgen/kgen.cpp),这意味着它同时支持上游mlir-opt的常见旗标:

# 打印某个 Pass 运行后的 IR(-all 表示所有 Pass) kgen -elaborate test.mojo --mlir-print-ir-after-all # 打印单个 Pass 后的 IR kgen -elaborate test.mojo --mlir-print-ir-after=inline-parametric # 打印 Pass 统计信息 kgen -elaborate test.mojo --mlir-pass-statistics

另外两个值得注意的选项是--save-temps--temps-dir=<dir>/<prefix>,二者必须配合使用,作用是把编译到某些阶段之后的 IR 保存成文件。其底层实现在编译选项中体现为saveTempsPrefixkgen--temps-dir的值写入options.saveTempsPrefix(见 Mojo/tools/kgen/kgen.cpp),随后由 Mojo/include/Mojo/Compiler/SaveAsmOutput.h 中的writeTempModule等辅助函数按<saveTempsPrefix><phase>.<hash><fileExt>的命名模式落盘。CompilationOptions中对应的设置接口是setSaveTemps(std::string prefix)(见 Mojo/include/Mojo/ToolCommon/CompilationOptions.h)。

⚠️ 关于--mlir-print-ir-[before|after]的关键限制:该选项只对"主"PassManager生效,即用于构建主流水线的那个 PassManager。Mojo 编译器以编译速度为核心目标,许多 Pass 会通过构造自己的嵌套PassManager来做激进并行,而这些嵌套 PassManager 并不继承主 PassManager 的选项(例如不经过applyPassManagerCLOptions)。因此:

  • --mlir-print-ir-[before|after]不会为这些嵌套 PassManager 打印 IR;
  • 同样地,它也不会为 offload(例如 GPU)目标打印 MLIR。

这意味着在调试 GPU 相关问题时,不能依赖--mlir-print-ir-after观察 offload 端的中间表示(见下文"Apple GPU 调试")。

kgen-opt:单 Pass 实验台

kgen-opt相当于 Mojo 编译器自己的mlir-opt,可对给定的 MLIR 输入运行单个 Pass。其实现位于 Mojo/tools/kgen-opt/kgen-opt.cpp,本质上包装了上游 MLIR 的MlirOptMain,并额外注册了 KGEN 方言与全部 KGEN Pass(KGEN::registerDefaultKGENPasses("kgen-opt")),还注册了DebugInfo::registerTransformsPasses()

用法示例:

# 对某个 MLIR 文件单独运行 inline-parametric Pass kgen-opt -passes="inline-parametric" dump.mlir -o out.mlir # 通过 --asyncrt-single-thread 强制单线程(kgen-opt 特有选项) kgen-opt --asyncrt-single-thread -passes="inline-parametric" dump.mlir

从源码看,kgen-opt还内置了一个调试辅助 Pass:test-always-fail(一个总是失败的 Pass,用于测试 crash reproducer),可用来验证故障复现流程(见 Mojo/tools/kgen-opt/kgen-opt.cpp)。

⚠️ 注意:对于会自行构建嵌套PassManager与子管线的 Pass,可能需要给其对应的 CL 选项追加额外参数才能生效。

kgen-llvm-opt:自定义 LLVM 优化管线测试

kgen-llvm-opt用于测试 Mojo 的自定义 LLVM 优化管线。与 LLVM 的opt不同,它目前不支持单独测试某个 Pass(但支持-passes=语法,见下)。从 Mojo/tools/kgen-llvm-opt/kgen-llvm-opt.cpp 的头部注释看,它有两种工作模式:

  1. 完整 KGEN 优化管线:通过-O0/-O1/-O2/-O3运行对应优化级别的完整 Mojo 编译管线;
  2. 单 Pass 模式:通过-passes=...指定 LLVM pass 管线语法(与opt -passes=...相同),例如kgen-llvm-opt -passes="kgen-llvmir-downgrade" in.bckgen-llvmir-downgrade用于把 IR 降级以兼容更老的 LLVM 后端)。

此外还支持--disable-optimization-passes:禁用优化 Pass 并打印输入模块,用于观察未经优化的 IR。这在 Apple GPU 调试中尤为重要(见下文)。

llvm-module-splitkgen-reduce

  • llvm-module-split:允许测试把 LLVM IR 拆分到不同模块的 splitter(Mojo 编译器在并行编译时会用到模块拆分,见"Tips"一节);
  • kgen-reduce:仓库文档标注NOT TESTED,其目标是把 MLIR 归约成更小、更易调试的形式,但当前似乎无人使用。

调试方法论:定位"肇事 Pass"

与 LLVM 不同,Mojo 编译器目前(文档撰写时)还没有类似-opt-bisect-limit的自动二分定位选项。因此定位问题 Pass 主要靠人工操作:

  1. 先找出导致问题的 Pass。由于缺少 bisect 选项,可能需要手动摆弄编译管线(例如通过kgen--mlir-print-ir-after=<pass>逐步缩小范围),找到第一个产出异常 IR 的 Pass。
  2. 警惕嵌套 PassManager 及其中的 Pass:嵌套 PassManager 中的 Pass 不会被--mlir-print-ir-before/after覆盖(见上节),遇到这类 Pass 需要另想办法,例如临时加op.dump()
  3. 确定肇事 Pass 后,用kgen转储 IR
  4. kgen-optopt或其他上述工具对 IR 做进一步实验,找出根因。

⚠️ offload 编译的特殊情况:如果需要观察KGEN -> LLVMPass 之前的 MLIR(即 offload 目标端尚未降级的中间表示),目前只能手动在 Pass 里加上op.dump()之类的调试输出,并建议用-workqueue=single-thread关闭多线程以保证输出顺序与稳定性。

实战 Tips 与构建配置

文档给出了几条实战经验,均可在仓库中找到对应的实现依据。

1. 运行时失败优先用release编译器复现

如果用户程序在运行时失败,先用release配置构建的编译器复现:

br //:install --config=release

原因productionbr //:install --config=production)构建会关闭所有验证与断言(assert),因此可能生成无效 MLIR 而不报错。release构建保留了验证与断言,更早暴露问题。这一结论与源码中KGENPassCLOptionsMODULAR_PRODUCTION宏下把可选项全部退化为默认值的行为相互印证(见 Mojo/include/Mojo/ToolCommon/CLOptions.h):生产构建去掉了运行期选项解析等开销,同时也去掉了大量防御性检查。

2. 不要用debug构建跑kgen

除非正在调试 Mojo Parser 本身,否则不要用debug构建(br //:install --config=debug-modular--config=debug-everything)来跑kgen工具。

原因debug构建非常慢。用于 Parser 调试之外只会浪费时间。

3. 通过KGEN_OPTIONS注入 Pass 级选项

部分 Pass 有专属的 CL 选项,可以经KGEN_OPTIONS环境变量传给mojo工具:

KGEN_OPTIONS="-kgen-parametric-inline-threshold=35" mojo build test.mojo

这要求先在mojo源码中定义KGEN_ENABLE_PASS_OPTIONS并重新构建。支持的选项定义在KGENPassCLOptions中(见 Mojo/include/Mojo/ToolCommon/CLOptions.h)。从该类的实现可以提取出当前支持的选项清单(含默认值):

选项类型说明默认值
kgen-automatic-inline-thresholduint64自动内联函数的阈值,优先级高于 Pass 自身启发式无(需显式指定)
kgen-parametric-inline-thresholduint64内联 parametric 函数的阈值,优先级高于 Pass 自身启发式无(需显式指定)
kgen-parametric-inline-avg-loop-trip-countuint64InlineParametricPass 启发式中估计的循环平均迭代次数4
kgen-stack-reuse-promote-to-global-thresholdsize_t只读栈分配被提升为全局变量的字节阈值1024
kgen-verifier-max-errorssize_tKGENVerifierPass 单次最多输出的错误数10

这些选项的消费者散落在 Mojo/lib/Transforms/InlineParametric.cpp、Mojo/lib/Transforms/AutomaticInline.cpp、Mojo/lib/Transforms/StackReuse.cpp 与 Mojo/lib/Transforms/KGENVerifier.cpp 等 Pass 实现中;kgenkgen-optmojo-buildmojo-run等工具入口都会调用KGENPassCLOptions::registerOptions()完成注册。使用时按调试需要调整阈值即可(例如调低 parametric 内联阈值以观察内联决策对 IR 的影响)。

4. 怀疑 LLVM 模块拆分器时关闭并行编译

偶尔会出现与 LLVM 模块拆分器(splitter)相关的编译期或运行期问题。如果怀疑是它,尝试禁用并行编译(例如用-workqueue=single-thread)再复现。

5. 只构建调试所需工具

在全新工作区上,仍需要先跑一次br //:install --config=....来正确设置符号链接;之后通常只需增量重建需要的工具即可。例如:

br //:kgen-tool //:kgen-opt

该命令只构建kgenkgen-opt两个工具,避免全量构建带来的时间开销。

用调试器调试 Mojo 编译器本身

当问题出在编译器自身逻辑(而非生成的 IR)时,需要用调试器单步跟踪。macOS 上lldb是最简单的选择;Linux 上可用gdblldb

有三种启动调试器的方式:

  1. 对 Bazel 测试目标
bd [--gdb] [--config=...] //<bazel-test>
  1. 对工具加参数--之后是传给被调试工具的选项):
bd --config=debug-modular --gdb KGEN/tools/mojo -- build \ --target-accelerator="amdgpu:gfx942" test.mojo
  1. 直接调用lldbgdb。注意两点:
    • debuginfo 包含相对路径:因此在 Mojo 工作区之外运行调试器将无法显示符号;
    • 如果想保留工作区之外的测试目录,可以使用下面的 shell 别名,把文件参数解析为绝对路径后再进入MODULAR_PATH目录启动调试器:
alias blldb='blldb_() { if [[ -z ${MODULAR_PATH} ]]; then echo "${MODULAR_PATH} must be set" fi args=(); for arg in $@; do if [[ -f ${arg} || -d ${arg} ]]; then args+=($(readlink -f ${arg})) else args+=(${arg}) fi done echo "(cd ${MODULAR_PATH}; lldb ${args})" (cd ${MODULAR_PATH}; lldb ${args}) unset -f blldb_; } blldb_' alias bgdb='bgdb_() { if [[ -z ${MODULAR_PATH} ]]; then echo "${MODULAR_PATH} must be set" fi args=(); for arg in $@; do if [[ -f ${arg} || -d ${arg} ]]; then args+=($(readlink -f ${arg})) else args+=(${arg}) fi done echo "(cd ${MODULAR_PATH}; gdb ${args})" (cd ${MODULAR_PATH}; gdb ${args}) unset -f bgdb_; } bgdb_'

这两个别名存在一些显而易见的边界情况(例如路径含空格时),但在把测试文件放在工作区之外的日常场景下非常实用。

Apple GPU 调试专章

Apple GPU 没有 LLVM 上游支持,Mojo 对它的支持来自逆向工程(reverse engineering),因此其调试流程非常特殊。整体思路是:Mojo 编译器先把 LLVM IR 转换成AIR(Apple Intermediate Representation)兼容的 LLVM IR,再交给 Metal 编译器(通过air-*工具链)生成最终内核。

核心命令组合

文档推荐的最佳命令组合如下:

# 生成转成 AIR 之前的 LLVM IR 文件,以及生成的 AIR 文件 kgen -elaborate --save-temps --temps-dir # 观察 Metal 编译器对给定 AIR 文件的行为 xcrun -sdk macosx air-* # 得到 AIR 转换之后的 LLVM IR kgen-llvm-opt -O3 -S # 不做任何 "LLVM IR -> AIR 兼容 IR" 的变换,直接从 LLVM IR 产出 AIR 文件 kgen-llvm-opt --disable-optimization-passes -O3

若想直接在运行时测试某个手工修改过的 AIR 文件:

KGEN_OPTIONS="-kgen-object-compiler-use-custom-air=<your-air-file>" mojo \ build/run test.mojo

配合llvm-reduce可以把 LLVM IR 归约到最小复现规模。

为 Apple GPU 添加 NVidia/AMDGPU 的某个特性 X

这类工作的第一步是理解 Metal 如何支持该特性。可以借助 LLM 或 Metal Shading Language 规范写一个 metal kernel,然后编译它并转储 LLVM IR,观察特性 X 是如何实现的:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/metal \ -mllvm -print-after-all test.metal &> dump

之后再把对应实现移植进编译器和/或 stdlib。

问题一:Invalid bitcode(metallib compilation failed with code

Metal 编译器仍使用 LLVM 17.0,因为它依赖 typed pointers 与 5.0 bitcode reader。Mojo 编译器自带一个应当与 5.0 兼容的 bitcode writer,但两者之间可能存在差异。

如果看到metallib compilation failed with code,说明生成的 bitcode 对 Metal 无效。排查步骤:

kgen -elaborate test.mojo --save-temps --temps-dir=/tmp/apple_gpu xcrun -sdk macosx air-objdump --disassemble /tmp/apple_gpu.*.air

air-objdump --disassemble会打印 Metal 编译器不接受的 opcode。如果仍不明确问题所在,用llvm-reduce配一个归约脚本缩小测试用例:

! kgen-llvm-opt --disable-optimization-passes -O3 $1 -o /tmp/kernel_$$.air || exit 1 xcrun -sdk macosx air-objdump --disassemble /tmp/kernel_$$.air \ -o /tmp/metalllib$$.metallib &> /tmp/reduce_$$.log grep -q "<the error you care about>" /tmp/reduce_$$.log || exit 1

然后执行归约:

kgen-llvm-opt -O3 -S /tmp/apple_gpu.pre-split*.ll -o /tmp/apple_gpu.kernel.ll llvm-reduce --test=reduce.sh /tmp/apple_gpu.kernel.ll

问题二:Incorrect output(错误输出)

这是最耗时的调试问题。目前已知会导致错误输出的两类根因:

  • loadstore上的 address space(地址空间)不正确;
  • 某个MTL::Buffer没有被传给 encoder。

最佳策略是手动摆弄 AIR 文件,找出具体是哪条(或哪些)指令不对:

kgen -elaborate test.mojo --save-temps --temps-dir=/tmp/apple_gpu kgen-llvm-opt -O3 /tmp/apple_gpu.pre-split.*.ll -S -o /tmp/kernel.ll

此时/tmp/kernel.ll包含的是已转换为 AIR 兼容形式的 LLVM IR。接着在运行时验证它:

kgen-llvm-opt --disable-optimization-passes -O3 /tmp/kernel.ll \ -o /tmp/apple_gpu.air clear-cache KGEN_OPTIONS="-kgen-object-compiler-use-custom-air=/tmp/apple_gpu.air" \ mojo run test.mojo

随后就可以逐条修改 AIR,定位到底是指令、属性还是 metadata 出错。

问题三:Failed to create compute pipeline state

这是目前最不明确的问题。它发生在 AsyncRT 试图获取需要传入MTL::Buffer的参数个数时——也就是说 Metal 编译器认为生成的 AIR 文件没问题,但反射(reflection)阶段仍然出错。

目前的最佳实践是组合 Invalid bitcode 与 Incorrect output 两节的方法,并为llvm-reduce编写更复杂的归约脚本。

⚠️ 注意:由于这类问题必须实际运行程序才能复现,llvm-reduce归约过程中某些测试可能出现无限递归或死循环,务必给运行二进制加上 timeout。

总结

Mojo 编译器的 Post-Parser 调试本质上是"在不同编译阶段截停并检查中间表示"的过程:

  • kgen按阶段截停编译并导出 MLIR / LLVM IR / bitcode / 汇编;
  • kgen-opt对单个 Pass 做最小化实验;
  • kgen-llvm-opt验证自定义 LLVM 优化管线(含 AIR 转换);
  • llvm-module-split/llvm-reduce处理模块拆分与用例归约问题;
  • 通过KGEN_OPTIONS注入 Pass 级选项、通过bd/blldb/bgdb在调试器里跟踪编译器本身。

需要特别记住的三条经验:--mlir-print-ir-before/after对嵌套 PassManager 与 offload 目标无效;生产构建会关闭验证与断言,调试时优先用release构建;Apple GPU 的调试必须理解 LLVM IR → AIR → Metal 工具链这条特殊路径。掌握了这套方法论,无论是定位错误降级、内联策略问题,还是排查 GPU 后端的 bitcode 兼容性,都能做到有迹可循。

延伸阅读

  • MojoCompilerWalkthrough.md:编译器整体流程讲解
  • Compiler.md:Support 库中的编译期支持设施
  • Mojo 编译流水线测试:kgen相关集成测试
  • kgen-llvm-opt 工具说明:两种工作模式与 Pass 列表
  • KGENPassCLOptions 定义:可注入的 Pass 级选项清单

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询