☰
Mojo 生成式内核编译器(kgen)任务清单全解析:从元编程 IR 到内核搜索与生成
2026/10/1 0:33:37 网站建设 项目流程

Mojo 生成式内核编译器(kgen)任务清单全解析:从元编程 IR 到内核搜索与生成

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

导读

本文基于 Modular 仓库中一份历史性技术文档《Generative Kernel Compiler Task List》展开。该文档是 kgen(kernel generator,生成式内核编译器)项目早期的工作分解清单,以"待实现 / 待规划 / 已完成"三段式记录了构建一套"用编译器生成高性能内核"体系所需的全部技术组件——从可带参数的kgen.generator元编程 IR,到meta.buffer参数化类型、generator 接口与约束系统、elaboration(展开)算法,再到 JIT 执行与性能测量基础设施。文档中标注为"已完成"的每一项,都能在当前仓库中找到真实的源码落点(KGENDialect、Elaborator、POPDialect、LowerLIT 等目录)。读完本文,你将理解 Mojo 编译器如何描述"内核的元程序"、如何穷举展开所有内核变体、如何用约束剪枝搜索空间,以及这些设计与仓库中具体源码文件的对应关系。

一、文档背景:一份"生成式内核编译器"的工程化路线图

这份位于 Mojo/docs/compiler/attic/TaskList.md 的文档,自述是 Generative Kernel Compiler + Language 设计文档的实现任务清单,用于把宏大设计拆解成可执行的工程粒度。它被放在attic(阁楼)目录下,从标题中的 "(OLD)" 可以看出,它已属于历史参考材料——文档中"Tasks to be implemented"一节里还标注为未完成的任务(如目标机器建模、memset/erf 生成器、搜索缓存),在后来的演进中大多已通过不同方式落地。

整份文档的核心思想可以概括为一条主线:

让内核(kernel)本身变成"程序生成程序"的过程——开发者编写带参数的 generator(生成器),编译器(elaborator)根据目标硬件与参数约束,自动展开并生成所有合法变体,再通过实测选择最优实现。

这与传统手写汇编/SIMD 内核、或仅靠 LLVM 自动向量化的思路都不同:kgen 把"选择哪个内核变体"从编译期决策升级为可搜索、可测量、可缓存的显式过程。

二、文档结构总览:三段式任务编排

文档采用"三段式"结构组织任务,这种编排本身就是项目治理方式的体现:

段落状态标记含义
Tasks to be implemented❌已有基本范围划分但尚未完整实现
To be scoped✅ / ⚠️已确认(✅)或待定(⚠️)的远期方向,尚未细化
Tasks completed✅已完成,按自底向上的顺序给出技术组件总览

文档还明确了工作流转规则:"当某个大方向完成时,应把任务移到文档末尾的 Tasks Completed 一节"。这种"未完成→已完成"的移动机制,使文档成为一份可演进的活文档——从仓库现状看,绝大多数 ✅ 任务都已在 Mojo/lib 与 Mojo/include/Mojo 中找到了对应实现。

三、核心概念与 IR 设计(已完成部分)

"Tasks completed"一节按自底向上顺序列出了主要技术组件,其中最关键的是元编程参数基础设施——这是整个 kgen 体系的地基。

3.1 新库/框架:kgen 的命名由来

文档记载,项目的第一步是在 Modular 仓库中新建一个遵循 include/lib/tools/test 标准结构的顶层目录。命名过程颇为有趣:

  • "corn"/"popcorn":因涉及 kernel 有 emoji 可用,但"听起来很蠢"被否决;
  • Abdul 建议 "kgen"(kernel generator 的缩写);
  • Tatiana 建议 "kir"(kernel intermediate representation)。

最终敲定kgen,且这个名字同时被用作一个方言(dialect)的名字。从仓库看,这一决策完整落地:存在 Mojo/lib/KGENDialect(19 个 .cpp 实现文件)、Mojo/include/Mojo/KGENDialect(Ops/Attrs/Types 的 TableGen 定义),以及独立的 Mojo/tools/kgen/kgen.cpp 工具。

3.2 元编程参数基础设施:kgen.generator 与参数表达式

文档指出,kgen 的核心需求是"在同一个 IR 中同时描述程序和元程序"。为此需要一系列 IR 构件,文档逐条列出(均已完成):

  1. kgen.generator操作:类似函数的操作,但允许携带参数。参考先例是 CIRCT 的hw.module。该操作在 KGENOps.td 中以def KGEN_GeneratorOp : KGENOp<"generator", ...>定义。
  2. 参数表达式:以简单的二元表达式和参数声明引用起步,例如#kgen.param.expr<add, ...>,需要实现代数规范化。仓库中的 KGENParameters.cpp 与 ParameterEvaluator.cpp 承担了参数声明与求值职责。
  3. 漂亮的打印/解析语法:文档作者明确不满意 CIRCT 参数属性的冗长打印(如#hw.param.expr.add<...>套娃),要求在大量测试用例落地前把语法糖设计好。当前仓库的语法已经简化为前缀调用形式,例如测试文件 kgen-param-exprs.mlir 中的:
%0 = kgen.param.constant = <add(p1, 42, mul(p2, p2))> %3 = kgen.param.constant: i1 = <to_builtin(:scalar<bool> eq(42, p1))> %6 = kgen.param.constant: i1 = <to_builtin(:scalar<bool> identical(:dtype type, f32))>

该测试通过kgen-opt -verify-parameters校验参数表达式并做常量折叠(例如eq(41, 42)折叠为0、identical(bf16, f16)折叠为0)。 4.kgen.param.constant操作:把参数投影为 SSA 值,使元程序中的值能被程序使用。 5.参数返回能力:允许 generator 返回参数(如白皮书中的panelDotInner),类似hw.output但允许参数列表作为属性。 6.参数类型系统决策(Issue #983):允许任意类型的参数,未指定类型时默认是带符号解释的 index 类型——这保证投影到 SSA 域后与架构无关,并能与 SCF 方言良好配合。语法上两种写法等价:

kgen.generator @foo<p1, p2, p3>(...) kgen.generator @foo<p1: index, p2: index, p3: index>(...)
  1. kgen.call(Issue #960):支持 generator 调用其他 generator,并带校验。
  2. 局部参数声明与绑定(Issue #966):让 IR 更具表达力和可读性,便于内核规模增长后的维护。

文档特别强调:表达式规范化、pretty print/parse 逻辑、以及尽早建立的 verification 逻辑是让后续所有工作变轻松的关键。仓库中 KGENAttrsFolders.cpp、KGENOpsFolders.cpp 正是表达式折叠/规范化的落地实现。

3.3 DType 参数的类型系统:!kgen.dtype

参数列表里 dtype 极为常见,但 MLIR 参数系统允许任意类型,如何处理 dtype 就成了问题。文档否决了"用带魔数的i8参数表示 DType"这种简单但难读的方案,改为引入:

  1. 新的!kgen.dtype类型(对应 KGENTypes.td 中的def KGEN_DTypeType : KGENType<"DType", "dtype", ...>);
  2. 允许参数列表中出现 0..255 整数初始化器、直接对应 DType 枚举值的属性;
  3. 让printParamValue对f32这类参数名加引号打印("f32"),使其像 MLIR 关键字一样不能作为裸词;
  4. 为f32等常见类型引入特殊"关键字",在解析时映射到对应 DType 值。

最终效果是下面这种可读性极佳的语法:

kgen.generator @foo<t1: !kgen.dtype>(... kgen.generate @foo<t1: !kgen.dtype = f32>(...

进一步(Issue #980),结合 kgen 类型上下文还可以省略!kgen.前缀:

kgen.generator @foo<t1: dtype>(... kgen.generate @foo<t1: dtype = f32>(...

3.4 新参数化类型:meta.scalar / meta.buffer / meta.simd / pop.pointer

有了参数化系统,下一步是构建参数化类型(参考 CIRCT 的hw.Int、hw.Array):

类型含义对应 Issue
meta.scalar<x>具体标量类型,x 为 dtype#978
meta.buffer<size, x>缓冲区(memref 的类比),size 为 index、x 为 dtype#1009
meta.simd<size, ty>SIMD 类型,size 可为任意参数表达式—
pop.pointer<T>无界指针运算#1663

配套基础设施包括:参数作用域校验(#979)、与具体类型互转的 cast 操作(#1221)、meta.buffer.address操作(#1249)。

文档特别强调了一条重要设计原则:通常不需要为 meta.scalar / meta.simd 类型定义算术操作——这些操作本身就应该是内核(kernel)。也就是说,加法的实现路径是"写一个生成加法的 generator",而不是在 IR 里内置一个特权fadd指令。

3.5 动态 shape 与 dtype:buffer

除编译期已知的参数化类型外,meta.buffer还必须支持动态大小与动态元素类型(Issue #1082)。MLIR 中可以用 null 参数表示未知,例如:

kgen.generator @fillWithOnes(%dest: !buffer<?, ?>) {} kgen.generator @knownFloatAlgo(%dest: !buffer<?, f32>) {} kgen.generator @fixedSizeUnknownAlgo(%dest: !buffer<1024, ?>) {}

该能力只适用于 buffer 类型——文档明确反对在 scalar/simd 上支持动态 dtype,也反对 simd 支持动态长度(理由指向仓库中的 Mojo/docs/compiler/manual/Rationale.md)。配套的运行时提取操作(Issue #1083/#1084)可以把参数提取为 SSA 值,并做 buffer 类型间互转:

kgen.generator @algo(%dest: !buffer<?, ?>, %other : !buffer<1024, f32>) { %dtype = meta.buffer.dtype %dest: !buffer<?,?> // returns !kgen.dtype %size = meta.buffer.size %dest: !buffer<?,?> // returns index %buf1 = meta.buffer.cast %dest: !buffer<?,?> to !buffer<?, f32> %buf2 = meta.buffer.cast %other: !buffer<1024xf32> to !buffer<?, f32> }

其中meta.buffer.dtype返回对应 DType 枚举的 i8 值,meta.buffer.size返回对应 size_t 的 index 类型;当参数为已知常量时应提供fold()折叠。

3.6 Generator 接口声明与实例

设计的另一个关键点是:一个 generator 接口可以有多个实现。因此需要把接口声明与实现分离:

  • kgen.generator.interface声明接口(Issue #1086),generator声明需标明自己实现哪个接口,并有类型检查保证声明/实现兼容;
  • 调用点把具体信息(如具体 dtype)绑定到接口声明侧的泛型参数上,通过传递参数 +meta.buffer.cast重化(reify)具体类型。文档给出了如下示意:
kgen.generator.interface @thing<dtype=?>(%dest: !meta.buffer<?xdtype>) ... %dstCast = meta.buffer.cast %dest to buffer<?xi32> meta.call @thing<i32>(%dstCast)

3.7 约束系统:用布尔表达式剪枝生成

generator 是"以参数为输入产生内核"的参数化函数,但它可能不是全函数——某些输入对它是非法的。约束(constraints)机制允许定义布尔条件来控制 generator 对其输入参数的有效性,从而让 elaborator 在展开早期就把不满足约束的 generator 剪掉,避免浪费搜索时间(Issue #1087):

constraints <in(vecLen, 2,4,8,16,32), notequals(type, bf16)>

文档预告,将来表达式语法可迁移到中缀形式(Issue #1395),如constraints <vecLen ∈ {2,4,8,16,32}, type != bf16>。generator 接口同样支持约束,用于统一约束其全部实现。配套能力还有:

  • elaborator 使用约束剪枝生成(Issue #1396);
  • 生成失败时的"展开栈回溯"错误报告机制(重构诊断与追踪);
  • kgen.param.assert:针对任意参数表达式(包括非直接 generator 参数)的广义断言。

四、编译器 elaboration 算法:穷举展开所有变体

有了描述能力(多实现、基本参数、约束),还需要执行展开的编译器基础设施(见 Mojo/lib/Elaborator/Elaborator.cpp 及其辅助文件)。文档明确:初期不引入搜索,而是穷举生成所有可能的内核实现。展开算法的步骤草图:

  1. 实现新的kgen-generate工具:读取含生成式内核的 MLIR 文件 + 库文件并加载;
  2. 校验源 MLIR 文件与库文件中的内核接口在签名等细节上一致;
  3. 处理内核体内的调用与其他操作,折叠参数表达式;
  4. 以SCC 顺序(自底向上)遍历 generator 调用树,把环检测为错误(库文件中的实现视为同一图的一部分);
  5. 自底向上生成完全特化的内核实现,写入目标 MLIR 文件(库文件保持不被修改),结果应完全特化、参数全部消除;
  6. 为每个内核跟踪绑定,确保接口调用点的多向展开方向一致。

从仓库看,kgen-generate工具如今演化为功能更完整的 Mojo/tools/kgen/kgen.cpp,其命令行支持--elaborate(仅展开并输出 MLIR)、--emit-llvm、--emit-assembly、--emit(输出 .o)、--execute等命令,并在runToolPipeline中调用compiler.runKGENPipeline(*theModule, target)。展开后的kgen.func即"已被展开的 kgen.generator"(KGENOps.td 中对kgen.func的注释如此说明)。

五、内核执行与性能测量基础设施

展开出大量变体后,必须回答"哪个是好的生成内核"。文档规划了两条执行路径(Issue #1610 的 JIT、Issue #1611 的编译为 .o 再执行):

  • 接入 LLVM 生成 .o 文件,或 JIT 到缓冲区后执行;
  • 运行需要输入生成基础设施与"预期 dtype"元数据;
  • 收集真实数据进行对比,例如 mmperf 的 matmul 维度列表、memset/memcpy 的"长度直方图"数据集。

在 Mojo/tools/kgen/kgen.cpp 中可以确认这条链路的实现:ObjectCompiler(Mojo/lib/Compiler/ObjectCompiler 相关)负责编译,ExecutionEngine(Mojo/lib/ExecutionEngine)负责 JIT 执行,且支持通过-func指定要执行的函数、通过-ignore-failure容忍失败——这些正是"穷举生成所有变体 → 逐个运行测量"工作流所需要的 CLI 支撑。

六、方言分层:lit 与 pop

随着内核越写越多,直接在 kgen 层编写会变得痛苦。文档规划了两层方言来支撑:

6.1 高层 lit 方言

kgen 层应保持简单、便于分析与变换(elaborator、静态分析工具都工作在类型检查过的元程序上),因此需要一个会被脱糖(desugar)到 kgen的高层方言。具体任务包括:

  • lit.fn操作(Issue #1410):generator 实现接口时,可以用比接口更特化的签名,并据此推断约束;
  • 模块系统与kgen.include操作:涉及"分离编译怎么做""导入的东西是 kgen 还是 lit 抽象层次"等问题。

仓库中 Mojo/lib/LowerLIT(LowerLIT.cpp、ConcreteBindings.cpp、CheckLifetimes.cpp 等)正是该层级的实现,Mojo/lib/LITDialect 定义了 lit 方言本身。

6.2 参数化操作方言 pop

由于 LLVM 方言不支持参数化类型,为每个整数宽度手写重载(如 fadd 示例)对人是巨大负担且拖慢编译。解决方案是新增parametric operations(pop)方言,允许这样写:

kgen.generator @fadd<dt: dtype>(%lhs: !kgen.scalar<dt>, %rhs: !kgen.scalar<dt>) -> !kgen.scalar<dt> constraints type = f32, f64 { %res = pop.add %lhs, %rhs : !kgen.scalar<dt> kgen.return %res }

文档同时指出:每个要用的方言都要适配工作是"烦人"的代价,因此通用特化(general specialization)方法很重要,使我们能与 MLIR 生态中的任何方言对话;LLVM 方言足够特殊和重要,值得投入适配。另外,文档还建议借此机会重新考虑 signless 设计——pop 方言可采纳带符号类型(signful types),让类型泛型内核更好写(不必为 divs/divu 各写一份)。仓库中的 Mojo/lib/POPDialect(POPOps.cpp、POPTypes.cpp、POPLLVMFolder.cpp 等)即该方言的实现,且存在pop.dtype.to_ui8/pop.dtype.from_ui8(POPOps.td)这类连接 kgen 与底层整数世界的桥接操作。

6.3 全类型参数化(Issue #2504)

有了以上支撑,可以写对标量与向量元素类型都泛型的内核,但还不能写对标量与向量同时泛型的内核——这是另一种参数化形态,需要把 MLIR 类型本身作为参数值传入。这是文档中最后一条已完成任务,对应 KGENAttrs.td 中大量以!kgen.type承载 MLIR 类型参数的属性设计(测试文件 kgen-param-exprs.mlir 中即有mlirType: type与#kgen.type<!kgen.param<:type mlirType>>的示例)。

七、标准库自举:基础构件即内核

文档还提出一个哲学层面的要求:整个系统应"定义在自身之内",减少特权技术。基础标量与 SIMD 算子(如 fadd)应该用语言本身定义为内核,而不是特权指令。文档给出了用 LLVM 方言 + cast 操作实现 fadd 的雏形:

kgen.generator @fadd<dtype=f32>(%lhs: scalar<f32>, %rhs: scalar<f32>) -> scalar<f32> { %lhsc = meta.cast_to_builtin %lhs: scalar<f32> to f32 %rhsc = meta.cast_to_builtin %lhs: scalar<f32> to f32 %resc = llvm.fadd %lhsc, %rhsc : f32 %res = meta.cast_from_builtin %resc : f32 to scalar<f32> kgen.return %res }

选择 LLVM 方言的好处是可直接使用目标特定 intrinsics 与内联汇编;是否引入 arith 等更高层方言则需要评估。相关 Issue:标量与 SIMD 加法(#1088)、buffer 加法(#1089),而fillWithOnes(#1090)在当时被标记为 ❌ 未完成——这也印证了文档中"待实现"与"已完成"是动态流动的。

八、待实现与待规划方向

文档保留了三条未完成任务("Tasks to be implemented")和四条远期方向("To be scoped"),它们体现了 kgen 路线的完整野心:

8.1 未完成任务

  1. 目标机器建模(❌):generator 应允许因"与目标硬件无关"而失败(例如用了 VNNI 特性但目标芯片不支持,或向量长度不匹配)。文档作者明确表示:不应让 LLVM 替我们合法化不支持的向量长度,即使 LLVM 能做。
  2. 更有意思的生成器(❌):实现 memset 与 erf 的真实生成器,这需要实现数学库、memset 所需的"由搜索得到的决策树"、基于 dtype 的动态 switch、scf.for、buffer 分区操作等。
  3. 用动态规划缓存(❌):当系统成型后,搜索执行时间会成为问题。需要缓存内核、利用层级结构,这要求能把微内核从大内核中隔离出来单独评估,可能把输入数据约束(如长度直方图)"下推"给微内核,也允许用户自定义度量(如针对预期维度的 FLOPS)。

8.2 待规划方向

  • 多维张量(✅ 已确认,延后实现):1D 的 buffer 类型并未硬编码进系统,它只是类型 + 操作的组合;一旦证明该模式可行,就能为表格、树等更复杂的数据类型添加新类型与操作。
  • 量化类型与生成器支持(⚠️):可扩展平台允许重新讨论量化类型及其 codegen 方式,用简单方案重实现 Ruy/XNNPACK 内核是好的起点,但不在此前的里程碑范围内。
  • 花哨的搜索算法(⚠️):基础设施成熟后,可探索多种搜索与搜索空间剪枝、黑盒优化、ML 辅助搜索;当前阶段优先暴力搜索以集中建设其他基础设施,最终希望支持多个可插拔/模块化搜索算法。
  • 高阶生成器(⚠️):希望 generator 能接收 region,例如一个"vectorize"操作接收 region 与宽度并尝试向量化它(类似 ISPC 对任意代码做 SPMD 化)。这样可以把算法(如 erf)写成标量代码,直接从标量规范生成 SIMD 版本;scf.for之类的 lowering 原则上也能这样做。

九、仓库中的实现证据与延伸阅读

想深入源码的读者,可以沿着以下路径继续探索(均以仓库根目录为起点):

  • kgen 方言定义:Mojo/include/Mojo/KGENDialect/KGENOps.td、KGENAttrs.td、KGENTypes.td,以及实现 Mojo/lib/KGENDialect(参数求值见 ParameterEvaluator.cpp);
  • 展开算法:Mojo/lib/Elaborator/Elaborator.cpp 与 ParametricElaborator.cpp;
  • kgen 工具:Mojo/tools/kgen/kgen.cpp(支持 elaborate / emit-llvm / emit-assembly / emit / execute 等命令);
  • lit 与 pop 方言:Mojo/lib/LowerLIT、Mojo/lib/LITDialect、Mojo/lib/POPDialect;
  • 测试样例:Mojo/test/kgen/kgen-dialect/kgen-param-exprs.mlir(参数表达式解析/折叠)、kgen-types.mlir、kgen-verify-errors.mlir(校验错误路径);
  • 设计澄清:Mojo/docs/compiler/manual/Rationale.md(动态 shape 的设计理由)。

结语

从这份历史任务清单到仓库中的实现,kgen 的路线图展示了一条清晰的技术演化路径:先用参数化 IR 让"内核的元程序"可描述,再用 elaboration 算法穷举展开并配合约束剪枝,随后建立执行与测量基础设施来挑选最优变体,最后通过 lit/pop 两层方言让内核编写与类型泛型化变得可持续。文档中"❌ 未完成"的三项(目标机建模、memset/erf 生成器、搜索缓存)以及"⚠️ 待规划"的搜索算法与高阶生成器,则勾勒出 kgen 更远的野心:从"穷举 + 实测"走向"可插拔搜索 + 由标量规范直接生成 SIMD"。对于想理解 Mojo 编译器内核生成机制的读者,这份文档与上述源码目录互为印证,是最佳的起点。

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

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

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

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

立即咨询