技术分析报告 · luminal-ai/luminal · 2026-11-03

Luminal 拆解一个“用搜索代替启发式”的推理编译器

Luminal 想用 少量原语算子 + 非破坏式重写(e-graph) + 在真机上实测的搜索,代替“为每种模型 × 硬件 × 数据类型手写 kernel,再用一长串启发式 pass 去优化”的做法。下面拆开讲它在做什么、难在哪、怎么解,以及当前代码实际做到了哪一步。

源码基准 main@1cbb917(2026-10-29) Rust · Apache-2.0 / MIT GitHub ★ ≈ 3.0k 创建于 2023-07
结论速览
  1. 它是什么:一个 Rust 写的 AOT(提前编译)推理编译器。前端接 PyTorch(torch.compile(backend=…))或自己的 Rust 张量 API,后端目前有 CUDA(luminal_cuda_lite)、Metal 和一个 CPU 参考实现。
  2. 核心赌注:不写“什么时候该做什么优化”的规则,而是把所有等价的写法都塞进一个 e-graph,再在 GPU 上实测挑最快的。规则只需要回答“这样改对不对”,不用回答“这样改值不值”。
  3. 真正的难点不在想法,而在工程:e-graph 会爆炸、e-graph 里的“代价”不可加、每条重写都必须在所有形状 / dtype / 动态维度下严格正确、实测一个候选很贵。
  4. 当前状态:main 分支正在一次大重构(spec 里叫 “rejoin”)的中后期。新管线的正确性很扎实(与 PyTorch 对拍、形式化很重的 egglog 规则),但性能暂时远落后:H200 上 FP32 的 Llama3-8B 每 token 约 221 ms,没有注册融合 attention。README 里“~80% 理论峰值”“自动发现 FlashAttention”描述的是旧管线和公司的目标。
  5. 对你有用的地方:它把 cuBLASLt(旧管线还有 FlashInfer)当作搜索空间里的普通候选,这正好给出“手写 kernel 库在编译器里的位置”的一种答案;它的布局代数和 CuTe 思路相通。

01它要解决的问题

部署一个模型,要面对三个相乘的维度:模型结构(LLM、扩散、MoE、CNN……)、硬件(H100、B200、Apple GPU、AMD……)、数据类型(fp32、bf16、fp8、nvfp4……)。主流框架的做法是为高频组合手写 kernel(FlashAttention、FlashInfer、cuBLAS、CUTLASS 模板),再用编译器 pass 把模型“降”到这些 kernel 上。

Luminal 的 README 和 why.mdx 指出了两个问题:

传统:手写 kernel 库 + 启发式 pass 模型 × N 硬件 × M dtype × K ≈ N·M·K 个手写 / 模板 kernel 启发式 pass 管线何时融合?何时 tile? Luminal:小原语集 + 等价重写 + 实测搜索 任意模型 ~十几到几十个原语 每个后端:一组重写规则“原语模式 ≡ 我的 kernel / 库调用” e-graph:所有等价写法不删除任何一种 在目标 GPU 上实测挑最快的那一份
图 1 左边的工作量随“模型 × 硬件 × dtype”相乘增长;右边每加一个后端,只需要写“原语模式 ↔ 后端实现”的等价规则,组合交给搜索。
类比 · 菜谱 vs 食材

传统框架像一本越来越厚的菜谱:每道菜 × 每种灶具 × 每种口味都写一页,还得附上“什么时候该换哪一页”的说明。Luminal 只规定十几种基本食材和刀法(原语),每种灶具(后端)只说明“这几样刀法合起来,我有一招更快的做法”。具体怎么组合,让厨房试做几轮,用秒表决定。

02核心思路:三根支柱

① RISC 式的小原语集(HLIR)

README 说一切最终都归结为 15 个原语:Log2 Exp2 Sin Sqrt Recip、Add Mul Mod LessThan、SumReduce MaxReduce Iota Gather Scatter Cast。注意几个刻意的选择:只有以 2 为底的 Exp2/Log2(exp(x)=exp2(x·log2e)),没有减法和除法(a-b = a + b·(-1),a/b = a·recip(b)),矩阵乘也不是原语,而是“广播 → 逐元素乘 → 沿 k 求和”。

当前分支的逻辑层比 README 多:src/logical/op/ 下有约 36 个 Logical* 算子,增加了 Select、And/Or/Not、Floor/Ceil/Round/Trunc、TruncDiv/TruncRem、三种 scan 等。主要原因是对齐 PyTorch 的精确语义(整数取整方向、比特级 bool),见 associative-fold.md 里关于 prod = exp(sum(log x)) 在负数上出错的讨论。

② 非破坏式重写:e-graph(egglog)

所有优化都写成等价关系,而不是“替换”。e-graph 同时保存一个计算的所有等价写法,规则可以按任意顺序、反复触发,没有“先做哪个 pass”的问题。规则引擎是 egglog(Datalog + e-graph),Luminal 用自己的 fork:luminal-ai/egglog。

③ 以实测运行时间为代价函数的搜索

e-graph 里可能有天文数字种组合。Luminal 不写代价模型,而是采样候选、生成代码、在目标 GPU 上真跑,用遗传算法迭代。HN 上作者的话:“The best heuristic is no heuristic.”

另外两个配套原则:一切 AOT(整个网络是静态图,编译器有全局视野),以及原生的符号维度((s, 4096)、(b, h, w+3)),用分桶(bucket)为不同的动态取值范围分别搜索。

03整体管线

spec 里写着当前分支的管线是 recorder → egglog_core → runtime 自己负责的 extraction/search → bufferize → runtime。展开如下:

饱和阶段(每个程序一次) 前端torch.compile / Rust API 逻辑图(HLIR)Logical* 原语 + 符号形状 egglog 饱和逻辑 / 布局 / 后端规则 e-graph所有等价实现的压缩表示 内存剪枝超出显存预算的候选删掉 搜索阶段(每个动态维度 bucket 一次,循环) 采样 genome每个 e-class 选一个 e-node 提取 + DPS + bufferize得到带存储的执行计划 代码生成NVRTC kernel / cuBLASLt GPU 上实测预热 + 3 次计时 最优计划运行时按 dyn_map 分发 按耗时排名 → 变异 → 下一代(默认 8 代 × 每代 8 个)
图 2 上排只跑一次:把程序和所有规则饱和成一个 e-graph。下排是搜索循环:每轮从 e-graph 里“抽”出一份具体实现,编出来、跑一下、排名。
阶段做什么源码
前端PyTorch 走 torch.compile 导出 Core ATen IR,逐个 ATen 算子翻译成 Logical 原语;也可以直接用 Rust GraphTensor API 建图pytorch/…/translate/ · src/frontend/
逻辑 IR每个算子一个目录:构造器 + dtype / shape / 布局推导 + 代数重写,各是一个 .egg 文件logical/op/mod.rs · op/add/
egglog 核心整数表达式、形状、布局的“前导程序”(5000+ 行 egglog)egglog_preamble.egg
后端规则“逻辑模式 + 布局条件 ≡ 这个 CUDA kernel / 库调用”luminal_cuda_lite/src/ops/
提取给定 genome,遍历 e-graph 得到一张具体的数据流图extraction.rs · extract_with_genome
搜索采样、变异、实测、留下前几名作为后备cuda search.rs · search_implementations · search_support.rs
BufferizeMLIR 风格的 one-shot bufferization,决定谁原地写、谁要新分配、哪里要拷贝bufferize.rs · bufferize
代码生成 / 运行把几何参数当字面量写进 CUDA 源码,用 NVRTC 编译;库调用走 host op;用 CUDA Graph 回放kernels.rs · KernelOp · cuda_graph.rs
语义参考CPU 上的完整参考实现,所有后端都和它对拍luminal_reference/runtime.rs

04深入:e-graph 与 egglog

为什么“非破坏式”很关键

经典例子(来自 egg 论文):表达式 (a×2)/2,有两条规则:强度削减 x×2 → x≪1,以及消去 (x×y)/y → x。用传统 pass 管线时,如果强度削减先跑,表达式变成 (a≪1)/2,消去规则就再也匹配不上了。这就是 phase ordering 问题:结果取决于 pass 的顺序,于是只能写启发式来决定顺序和触发条件。

破坏式:改写后原式消失 (a × 2) / 2 (a ≪ 1) / 2 卡住:消去规则匹配不上 规则1:x×2 → x≪1 规则2 找不到 (x×y)/y e-graph:两种写法都留在同一个等价类里 ( · ) / 2 e-class a × 2 a ≪ 1 规则2 通过 a×2 这一写法匹配:(a×2)/2 ≡ a
图 3 e-graph 不“替换”,只“合并等价类”,所以规则的触发顺序不影响最终能到达的写法集合。代价是图会变大,这正是后面要解决的难点。
类比 · 下棋落子 vs 摆出所有棋谱

破坏式 pass 像下棋时落子无悔:每一步都必须当场判断好坏,走错了回不去。e-graph 像把所有合法走法都画在一张棋谱树上(而且相同局面只画一次),最后再挑一条线。规则作者只需要保证每一步“合法”,不必判断“好不好”。

一条真实的规则:把“广播-乘-求和”识别为矩阵乘

矩阵乘不是原语,它是一个模式。下面这条规则把这个模式和一个辅助节点 LogicalHelperMatrixMultiply 合并到同一个 e-class,后端规则只需要匹配这一个节点:

; src/logical/helper/matrix_multiply/recognize.egg
(rule
  ((= ?out  (LogicalReduceSum ?prod 0))
   (= ?prod (LogicalMul ?a_b ?b_b))
   (= ?a_b  (LogicalHelperBroadcastAxis ?a 1 ?n))
   (= ?b_b  (LogicalHelperBroadcastAxis ?bT 2 ?m))
   (= (shape-of ?prod) (ShapeLit [?m ?n ?k]))      ; 形状把两条腿绑到同一个 [m,n,k]
   (= (shape-of ?a)    (ShapeLit [?m ?k]))
   (= (shape-of ?bT)   (ShapeLit [?n ?k])))
  ((union ?out (LogicalHelperMatrixMultiply ?a (LogicalHelperMatrixTranspose ?bT))))
  :ruleset main_ruleset
  :name "helper/matrix_multiply/recognize: reduce-sum of broadcast muls is a MatrixMultiply")

关键字是 union,不是“替换”。另外有一条方向相反的 expand.egg,把矩阵乘展开回原语链。两个方向同时存在,正是非破坏式的意思。博客里提到一个好处:模型作者没有显式写 matmul 的地方,只要出现这个模式,也会被识别出来。

可交互:一个 e-graph,三种 genome

下图是上面矩阵乘的 e-graph。虚线框是 e-class(一组等价计算),实线框是 e-node(一种写法)。搜索里的 genome 就是“每个 e-class 选哪个 e-node”。点按钮看不同 genome 选出的实现:

e-class:out[m,n] ReduceSum(·, k) MatMul(a, b) CublasLt(A, B → D) e-class:prod[m,n,k] Mul(·, ·) Bcast(a, n) Bcast(bᵀ, m) 输入 a[m,k] bᵀ[n,k] 虚线框 = e-class:一组等价的计算 实线框 = e-node:其中一种写法 genome = 每个 e-class 选一个 e-node 没被选中的子图根本不会被提取

图 4 简化示意:真实 e-graph 里每个张量还带着布局(见下一节),cuBLASLt 节点还挂着描述符、精度证明等事实节点。

05深入:布局层(逻辑值 × 物理布局)

Luminal 把“算什么”和“数据在内存里怎么摆”分成两件事。逻辑 IR 里的张量没有物理布局(spec:只有输入输出有);在 e-graph 里,LayoutTensorLit(逻辑值, 布局) 把两者配对,同一个逻辑值可以有很多种布局写法,而且它们都在 e-graph 里互相等价。

BitOffsetExpression任意比特偏移表达式(可表达 bool 打包、子字节 dtype) ElementOffsetExpression按元素的偏移表达式(dtype 允许时从上一层发现) StridedElementshape + stride(零偏移时才能发现) RightMajorContiguous行主序紧凑 LeftMajorContiguous列主序紧凑
图 5 “向下发现”的布局阶梯(preamble 开头的清单):越里层越具体,越容易被硬件 kernel 直接使用。规则自动从通用表达式里“认出”更具体的形式。

后端规则只匹配硬件能直接用的那种写法。例如 CUDA 的 ReduceSum kernel 写的是 out[i],所以规则要求输出布局是行主序紧凑,并且映射是单射:

; crates/luminal_cuda_lite/src/ops/reduce_sum/match_functional.egg(节选)
(= ?out_layout (RightMajorContiguousElementLayoutLit ?out_rm_shape ?out_rm_bits))
(= ?out_logical (LogicalReduceSum ?in_logical ?reduction_dim))
(!= ?value_dtype (Int))                         ; 整数单独一条规则:要处理回绕语义
(= (injectivity-of ?out_layout_tensor) (Injective))
; ⇒ union 成 LayoutTensorOpReduceSumGeneric(一个具体 CUDA kernel)

注释写道:“The e-graph joins equivalent spellings; the matcher matches the hardware's literal one.” 也就是说:等价性交给 e-graph,硬件约束交给匹配条件。转置、切片这类“视图”在这一层只是布局的改变,可以零拷贝;需要实体化时再插入 MaterializeLayoutCopy,是否插入同样由搜索决定。

类比 · 同一本书的不同排版

逻辑值是书的内容,布局是排版:横排、竖排、双栏都是同一本书。e-graph 把所有排版都登记成“同一本书”;每台印刷机(kernel)只接受某一种排版的稿子。搜索要决定:直接用现成的排版送印,还是先花时间重排一遍(layout copy)再送去更快的印刷机。这和你在 G2 里学的 CuTe Layout(坐标 → 偏移的函数)是同一类思想。

为什么不像 egg 那样用代价函数做贪心提取?因为在 kernel 层面代价不可加:同一个 matmul,用 cuBLASLt 本身更快,但如果它要求的布局导致前后各多一次拷贝,整体可能更慢;融合、内存复用、launch 开销都是全局性质。Luminal 的选择是:不建模,直接测。

采样 genome独立 + 相关两种抽法 提取保证无环 规划DPS + bufferize + 显存预算 编译 + 预热NVRTC 模块缓存复用 计时 3 次超时只算运行时间 排名保留前 4 名作后备 变异每个后代 2 处点突变 下一代
图 6 CUDA 后端 CompileOptions 的默认值:8 代 × 每代 8 个候选、每个后代 2 处突变、3 次计时、保留 4 个后备。被拒的候选(提取失败、超显存、超时)分门别类计入 RefusalBreakdown。

几个细节值得注意:

// crates/luminal_cuda_lite/src/search.rs
impl Default for CompileOptions {
    fn default() -> Self { Self {
        generations: 8, generation_size: 8, mutations: 2, trials: 3,
        seed: 0, keep_finalists: 4, candidate_timeout: None,
        device_budget_bytes: None, /* 默认取可用 CUDA 显存 */ .. } }
}
类比 · 试驾,而不是读参数表

代价模型像读汽车参数表:马力、扭矩都很漂亮,但上了真实路况(布局拷贝、launch 开销、缓存)不一定快。Luminal 每一代挑 8 辆车去同一条赛道各跑 3 圈,留下最快的几辆,把它们的零件(每个 e-class 的选择)随机换掉两处,再跑下一轮。缺点也一样:试驾很花时间。

07深入:厂商库也是搜索空间里的普通候选

“不写手写 kernel”不等于“不用手写 kernel”。cuBLASLt 调用在 Luminal 里就是一种 e-node,和生成的通用 kernel 平起平坐,由搜索决定用不用。难点在于:库调用的参数空间(转置、主序、leading dimension、batch stride、精度、epilogue)很大,而且每种组合在什么条件下合法,必须被严格证明。

当前做法分四步,每步一组 egglog 规则(cublaslt/README.md):

  1. 算术识别:认出 D = α·op(A)·op(B) + β·C 加可选 epilogue(bias、ReLU、GELU)。这一步只是“命名”,不代表允许执行。
  2. 布局证明:证明某个存储张量确实按某个 vendor 描述符(7 个字段:dtype、主序、行、列、ld、batch 数、batch stride)寻址。
  3. 精度证明:只有作者显式写出的精确扩宽才允许直接读 BF16 / FP16;存储格式不能决定计算精度。
  4. 准入:上面这些事实再和白名单里的某一条完整配置对上,才生成可执行节点。白名单有 2,176 条完整配置,文件 approved_whitelist.egg 有 21,787 行。空白名单就什么都不生成。
类比 · 海关清关

认出“这是一箱电子产品”(识别)不等于可以入境。还要有装箱单(布局证明)、原产地证明(精度证明),而且商品编码必须在海关的准入清单上(白名单)。任何一张单据缺了,这箱货就留在仓库(不生成可执行节点),搜索就只能走通用 kernel 那条路。

和 FlashInfer 的关系:README 的“Where are we”里写着 “Many kernel libraries supported in the search space (FlashInfer, cuBLASLt, etc.)”,这是旧管线的状态。BRINGUP.md 把旧 crate 描述为一个“零件库”(cuBLASLt / FlashInfer / MoE、CUDA Graph),并说明新管线是按 CL-3 阶段逐个迁回来的。目前迁回来的只有 cuBLASLt。环境变量 LUMINAL_FLASHINFER_DIR 等仍留在 README 中。

08深入:内存规划与动态形状

DPS + one-shot bufferization

提取出来的图里,中间值还没有存储。Luminal 先把每个算子改写成“目标传递”风格(DPS:输出缓冲区作为参数传入),再用 MLIR 风格的 one-shot bufferization 决定原地复用还是新分配。冲突引擎检查三件事:可写性(不能写只读绑定)、写后读(所有读必须在 DAG 上可证明地先于覆盖写)、跨操作数共享许可(只有算子自己声明了 Sharing::May 才允许)。被拒的原地复用一律“修复”成新分配加必要的拷贝,所以拒绝永远不是错误。

类比 · 酒店排房

每个中间张量是一位住客,入住和退房时间(生命周期)事先都知道。排房员(bufferize)尽量让新客人住进刚退的房间(原地复用),但有两条铁律:房间还有人要回来拿东西(后续还有读)时不能让给别人,标着“禁止入住”的房间(只读输入)不能用。排不下就另开一间(新分配),需要的话先把行李搬过去(拷贝)。

显存剪枝

搜索之前,CUDA 后端会对 e-graph 做一次强制的内存剪枝:在整个声明的动态维度范围内,物理容量超过显存预算的张量连同它们的生产者实现一起删掉,边界张量不能删(egraph_postpass.rs)。这样搜索根本不会抽到“需要 200 GB 中间结果”的路线。这属于“删掉不可行的候选”,不违反“不在 Rust 里做模式匹配和选择”的规定。

符号维度与分桶

形状是整数表达式(c + q、ceil(n/128)),egglog 前导程序里有一整套带值域的整数代数:截断除法与向上取整除法分成不同构造器,除法只在能证明除数非零时才参与合并。运行时给出 dyn_map(如 {q:1, c:812}),按所在 bucket 分发到对应编译结果。spec 要求:在代表点上测出来的最优计划,必须在整个声明范围内都合法。

09自动“发现” FlashAttention:说法与现状

这是 Luminal 最出名的一句话:不靠手写算子,靠搜索自动找到 FlashAttention 这类复杂优化。按作者在 Show HN 帖子中的解释,路径是:重写规则里既有循环 / 分块结构的改写,也有代数改写,其中关键一条是 softmax → online softmax。两者组合之后,搜索空间里就出现了“按 KV 分块、在块间重缩放、S 矩阵不落显存”的写法,也就是 FlashAttention 的结构。

朴素写法(由原语直接拼出) S = Q·KᵀN×N 整块写回 HBM P = softmax(S)max → exp2 → sum → recip O = P·V再读一遍 N×N e-graph 里加入等价写法 FlashAttention 结构(同一 e-class 里的另一种写法) 循环 / 分块改写沿 KV 维度切块 softmax → online softmax运行最大值 m、和 l、块间重缩放 融合成一个 kernelS、P 只在片上,不落 HBM 实测把它挑出来:S 不再写回 HBM,内存流量从 O(N²) 降到 O(N·d)
图 7 根据作者描述画的路径。关键是 online softmax 这条代数等价(你在 T5 / L3 / F2 里亲手写过的 m、l 重缩放,以及 merge_state)一旦作为规则进入 e-graph,“分块 + 融合”就成了一个可以被提取的候选。
需要区分的地方:这个演示来自 2025 年的旧管线。当前 main 分支自己的测量报告写明 “no FlashInfer/fused attention operation is available”:attention 的 QKᵀ 和 AV 走通用 kernel,而且当前的矩阵乘识别规则只认 rank-2,GQA 的批量高阶乘积匹配不上,搜索再久也找不到(SEARCH_DIAGNOSIS.md)。另外,有第三方总结指出这个演示没有公开搜索预算和可复现的计时对比。

这也揭示了这条路线的一个本质约束:搜索只能找到规则能到达的写法。SEARCH_DIAGNOSIS 原话:“Increasing generations/population explores combinations of existing producers; it does not extend the rewrite vocabulary.” 所以能力上限取决于规则库,而“该写哪些规则”本身仍然需要人的洞察。HN 讨论中也有人指出,等价保持的搜索很难找到 Strassen 这类“换算法”的优化。

10难点与解法总表

难点为什么难Luminal 的做法源码 / 证据
e-graph 爆炸规则越多,等价类和节点越多;有的规则(如结合律、交换律)会无限生成新项规则全部放进命名 ruleset,并按阶段调度;整数代数有一整套“fence doctrine”控制哪些模式能命名哪些项,目的是让规则代价可控;旧 spec 里还有 loop rolling,把重复的层压成一个循环体再饱和spec.md · Rule conventions · preamble 的 DOCTRINE 注释
代价不可加kernel 的快慢取决于前后布局、融合与内存,传统的逐节点代价提取不准不建模:genome 采样 + 遗传算法 + GPU 实测;相关采样提高抽到连贯候选的概率sample_genome_correlated
每个候选都必须正确搜索会挑出任何一个可提取的图,只要有一条规则在某个边角形状、dtype 或动态维度下不成立,就会出错规则是“带前提的等价”:除法需要值域证明除数非零,整数有回绕规则,布局要证明单射;禁止在 Rust 里对提取后的图做后处理;CPU ReferenceRuntime 作为语义标准,PyTorch 对拍测试套件AGENTS.md · reference runtime
库调用的合法性cuBLASLt 参数组合多,每种在什么布局 / 精度下合法很微妙识别 → 布局证明 → 精度证明 → 白名单准入,分层证明,2,176 条配置cublaslt/egg/
布局与视图转置、切片、广播要零拷贝,又要让 kernel 拿到它能用的布局逻辑值与布局分离;布局阶梯自动发现;视图与实体化拷贝都是候选,由搜索决定layouts.rs
显存有些候选的中间结果在长上下文下会超出显存搜索前按完整动态范围剪掉不可能的实体化;候选 arena 分配前再检查一次egraph_postpass.rs
动态形状静态编译与运行时变化的 batch / seq 长度矛盾符号维度 + 分桶,每个 bucket 单独搜索,运行时按 dyn_map 分发bucketing.rs
搜索很贵每个候选都要编译 + 分配 + 预热 + 计时;作者在 HN 上说典型是 10 分钟到 10 小时NVRTC 模块缓存(编译时间不计入候选超时)、早停、保留后备;作者提到计划用 MCTS / RL 和按后端记忆化search-timing RESULTS

11现状:仓库自带的实测数字

以下数据来自仓库中的验证报告(2026-09-21,NVIDIA H200,batch 1,FP32,128 输入 / 128 输出 token,3 次热请求取中位数):

模型TTFT (ms)TPOT (ms/token)编译搜索 (s)cuBLASLt 实例通用 attention 乘积
Qwen3-0.6B842.9741.84163.1619456
Qwen3-4B3,524.11144.41189.6925472
Llama3-8B-Instruct5,847.96221.26252.8322964
Gemma3-4B-IT(文本)4,095.09167.18377.1321268

估算一下差距:Llama3-8B 的 FP32 权重约 8.03 B × 4 B ≈ 32 GB,H200 的 HBM 带宽约 4.8 TB/s,所以 decode 每 token 的带宽下限约 6.7 ms。实测 221 ms,约为下限的 33 倍,带宽利用率约 3%。这是我的粗略估算,不是仓库给出的数字,也忽略了 KV cache 读取,但量级足以说明:新管线目前关注的是正确性和架构,性能还没跟上。

搜索本身也很贵。仓库的一次计时分析里,Llama3-8B 的搜索用了 347 s,其中 60.5% 花在 pinned 主机内存的分配、清零和释放上,并不是在 GPU 上计时。下面是其中一个候选的耗时分解(按比例绘制):

提取 + 规划 0.28 s 准备 17.05 s 其中 pinned 分配 / 清零 ≈ 16.9 s 预热 2.89 计时 ×3 8.61 s 释放 6.39 合计 35.24 s 0 s35.2 s
图 8 Llama3-8B 搜索中候选 2.1 的耗时(search-timing RESULTS.md)。真正“测速”的 3 次计时只占约 1/4,大头是准备和释放这些工程开销。这是可以优化的部分,不是方法本身的上限。

正确性方面比较扎实:Metal 报告里,Llama3 / Qwen3 / Gemma3 / Qwen3-MoE 的小型实例与 ReferenceRuntime 的 logits 误差都小于 1e-4;CUDA 上四个模型 128 个 token 输出前后一致。README 中 “Q8 Llama 3 8B ~80% of theoretical max on H100”,以及 HN 帖子中 “M 系列 Mac 上 Q8 Llama 3 8B 15–25 tok/s”,对应的是旧管线(作者在 HN 上也承认当时还比不上 llama.cpp 这类手调实现)。

12与其他编译器 / 系统对比

系统表示怎么选实现和 Luminal 的关系
XLA / torch.compile (Inductor)HLO / FX 图,破坏式 pass启发式规则;Inductor 生成 Triton,再对 Triton 配置做自动调优Luminal 直接批评的对象:规则必须确定有收益才触发
TVM / AnsorTensor Expression + schedule代价模型 + 进化搜索,只针对单个算子的调度作者说 Luminal 是“autotuning on steroids”:搜索的是整图的等价结构,不只是单个 kernel 的参数
TASO / Tensat计算图替换;Tensat 用 e-graph代价模型 + ILP 提取最接近的学术前身;Luminal 用实测代替代价模型,把重写下沉到布局和 kernel 层
Mirage(OSDI ’25)多层 μGraph(kernel / block / thread)穷举超优化 + 概率等价验证同属“搜索出融合 kernel”;Mirage 搜子图结构,Luminal 靠重写规则生成空间
tinygrad极小原语集(UOps)BEAM 搜索 kernel 优化参数README 点名同路人:小原语集、懒执行;tinygrad 没有 e-graph
FlashInfer / cuBLAS / CUTLASS手写 / 模板 kernel 库人工 + 运行时 plan/选择在 Luminal 里是搜索空间中的候选 e-node,而不是对手

13评价与借鉴

做得好的

  • 问题拆分非常干净:规则只管“对不对”,搜索只管“快不快”
  • 正确性纪律严格:带前提的等价、禁止后处理、参考实现对拍
  • 厂商库作为候选,和生成的代码公平竞争
  • 大量设计决策写在源码注释和验证报告里,连负面结果也记录

风险与局限

  • 能力上限由规则库决定;attention / RoPE 的高阶乘积目前匹配不上
  • 当前分支性能远低于带宽上限,FP32 为主
  • 搜索时间以分钟到小时计,不适合频繁换形状、换模型的场景
  • egglog 规则体系门槛高,注释里大量内部决议,外部贡献者不容易进入

对你有用的

  • 把 online softmax / merge_state 写成“代数等价”的视角,可以用来讲解 FlashAttention
  • 布局阶梯 + 单射证明,和 CuTe layout algebra 相通
  • “库调用准入证明”的分层思路,可以借鉴到 FlashInfer 的 API 选择和测试
  • 面试中谈编译器 vs 手写 kernel 时,这是一个很好的反方案例
类比 · 地图和导航

手写 kernel 库是老司机记在脑子里的近路:快,但每个城市都要重新学。Luminal 想做的是导航软件:把所有合法道路都画进地图(e-graph),再实时测路况选最快路线(实测搜索)。导航能有多好,取决于地图画全了没有(规则库)和测路况花多少时间(搜索成本)。目前它的地图还缺几条高速公路(融合 attention),测路况也偏慢。

14源码阅读路线(约 3 小时)

  1. 想法(20 分钟):README → spec.md(注意它已过时,但契约部分仍然成立)→ AGENTS.md 的 Debugging 和 Rewrite Boundary 两节。
  2. 一个算子的完整生命周期(40 分钟):logical/op/add/ 的 10 个 .egg 文件 → CUDA 侧 cuda_lite/ops/add/ → KernelOp 怎么生成源码。
  3. 模式识别(20 分钟):matrix_multiply/ 的识别与展开 → cublaslt/README.md。
  4. 提取与搜索(40 分钟):Genome → sample_genome / mutate_genome → search_implementations。
  5. 内存(30 分钟):bufferize.rs 文件头的长注释(冲突引擎讲得很清楚)。
  6. 诚实的现状(20 分钟):SEARCH_DIAGNOSIS.md 和 BRINGUP.md。
  7. 动手:cargo test -p luminal --lib(不需要 GPU);在 5090 上可以试 cargo run --release -p luminal_cuda_lite --example llama3 --features device,加 LUMINAL_LOG=1 看搜索过程,加 LLIR_DUMP_DIR=… 导出选中的计划。