不到 10 天,用大模型“搓”一个编译器

在不到 10 天的时间里,和大模型一起,把九齿 Ninetoothed 从一个主要生成 Triton 代码的 DSL,推进成了一个具备 SSA IR、lowering pipeline、pass registry 和多后端代码生成雏形的编译器框架

这一过程最有意思的地方并非“大模型写了多少代码”,而是“大模型并非天然会做编译器”。若仅提出“支持 CUDA、TileLang、TVM 后端”的需求,它极易倾向于选择最直接却也最危险的路径——为每个算子编写独立的 Kernel 模板。

例如,遇到 matmul,就写 lower_matmul;遇到 reduction,就写 lower_reduction;遇到 flash attention,就写 lower_flash_attention。这样看起来很快能跑通几个 demo,但它不是编译器,只是一堆特化代码生成器。

真正的目标是:输入只有九齿代码,编译器需要理解计算过程,将其降低为通用 IR,随后逐步 lower 至不同后端语言,而非在中间环节识别“这是 attention”,然后替换成一个手写模板。

第一步:重新定义问题

一开始给大模型的需求很宽泛:九齿要支持 Triton、CUDA、TileLang、TVM 多个后端。但很快会发现,若问题定义不够精确,大模型往往会采用“工程捷径”,倾向于生成针对 Kernel 的字符串模板,使 add、matmul、reduction、attention 这些典型算子看起来能过。

于是需求约束被不断收紧:任务不再是编写几个后端 Kernel,不再是识别算子名称,也不再是引入 AttentionOpIR 这种粗粒度的 IR。

核心要求变为从九齿的 Python application 里抽象出计算过程本身,形成细粒度的 IR,涵盖算术、访存、shape、index、loop、if、reduce、dot、store 等基本操作。这才是编译器。

第二步:把“大模型写 kernel”的冲动拉回编译器路线

这个过程中最关键的纠偏环节,在于明确要求 IR 的粒度不能宽泛到“一个算子一个节点”。

也就是说,不应出现:

AttentionOpIR
FlashAttentionOpIR
MatmulKernelTemplate
lower_flash_attention

应当构建的是:

arith.add
arith.mul
tensor.extract
linalg.dot
reduce.max
reduce.sum
scf.for
scf.if
select.where
mem.store

这样,flash attention 不再是一个特殊算子,而是一段由 dot、mask、online softmax、循环状态和归约组成的计算图。

此时,大模型才开始真正沿着编译器的逻辑路径进行思考与代码生成。

第三步:引入 SSA IR

把九齿的 application Python AST lower 到 SSA 风格 IR。

一个最简单的 add:

def application(x, y, out):
    out = x + y

会变成类似:

ssa @elementwise_add {
  inputs:
    x : tensor<BLOCK_SIZE>
    y : tensor<BLOCK_SIZE>
    out : tensor<BLOCK_SIZE>
  outputs:
    out : tensor<BLOCK_SIZE>
  ^entry:
    %0 = arith.add x, y
    mem.store %0, out
}

matmul 则先表示成结构化 linalg:

%0 = linalg.matmul lhs, rhs
mem.store %0, output

后续再通过 pass lower 成 index、loop、extract、mul、add、store。

flash attention 的参考实现也能被拆成:

scf.for
scf.if
linalg.dot
linalg.transpose
reduce.max
reduce.sum
math.exp2
select.where
tensor.extract
tensor.view
mem.store

此时, IR 已不再依赖于“算子名称”,而是在表达计算本身,从而为后续的优化与后端适配奠定了基础。

第四步:设计 Pass Pipeline

有了 IR 之后,下一步是 Pass。

把优化与 lowering 划分为三类:

hardware-independent passes
hardware-dependent passes
backend-specific passes

默认 pipeline 类似:

ssa.canonicalize
ssa.analyze_effects
ssa.select_schedule
ssa.<backend>.optimize_schedule
ssa.decompose_linalg
ssa.lower_memory_scopes
ssa.lower_backend_intrinsics

每个 pass 都只做一件事:

  • canonicalize 确认 IR 进入 generic SSA 形态。

  • analyze_effects 分析是否有 dot、reduction、loop、store、attention-like exp-reduction-dot pattern。

  • select_schedule 根据 IR 形态选择 elementwise-grid、blocked-linalg、exp-reduction-dot-region 等调度粒度。

  • backend.optimize_schedule 为 Triton、CUDA、TileLang、TVM 分别附加后端策略。

  • decompose_linalg 把高层 linalg 下降成更低层 loop/index/tensor/arith。

  • lower_memory_scopes 加入 register/shared/global 的后端映射。

  • lower_backend_intrinsics 选择 tl.program_idblockIdx/threadIdxT.thread_binding 等目标后端概念。

第五步:统一后端代码生成

在此架构下,后端不再是:

if matmul: lower_matmul()
if reduction: lower_reduction()
if flash_attention: lower_flash_attention()

而是统一走:

lower_unified_ssa_artifact(kernel, backend)

区别只在 target spelling:

  • Triton 生成 tl.loadtl.storetl.where

  • CUDA 生成 pointer load/store、blockIdx.xthreadIdx.x、C++ loop。

  • TileLang 生成 T.prim_funcT.thread_bindingT.match_buffer

  • TVM 生成 TVMScript/TIR。

这才使得多后端支持开始具备泛化性。

第六步:测试和反复纠偏

在整个过程中,测试不仅用于验证正确性,同时也是防止大模型回归“模板化捷径”的约束。

测试策略包含多个维度:

  • 检查后端文件中是否存在特化的 Lowering 入口,如 lower_matmullower_flash_attention
  • 验证 add、matmul、reduction、rowwise、fused expression、attention 各类算子是否统一走 SSA Lowering 流程;
  • 确认四个后端是否能从同一份 SSA 生成可运行代码;
  • 检查 pass trace,确保每一层 IR 都是逐步 Lower 而来。

这些测试确保了交付成果不仅“看起来能用”,而且在结构上符合编译器的标准。

最有价值的经验

此次实践表明,大模型擅长代码生成,但对“系统边界”的理解往往不够深刻。若指令仅停留在“支持某功能”,模型倾向于寻找捷径;若目标是“设计编译器”,则必须持续引导其关注核心概念:

  • IR 是什么?
  • IR 的粒度应该多细?
  • Lowering 每一步增加什么信息?
  • Pass 是否语义清晰?
  • 后端差异应该在哪里进入?
  • 优化策略是写死在 emitter,还是沉淀成 pass?
  • 有没有测试证明这不是模板拼接?

通过明确这些约束,大模型得以从单纯的代码生成器转变为编译器开发的协作者。

最后沉淀成 Skill

此次开发过程还沉淀出一组面向编译器开发的 skill,例如:

  • compiler-ir-architecture
  • compiler-ssa-lowering
  • compiler-pass-pipeline
  • compiler-backend-emitter
  • compiler-lowering-audit
  • compiler-multibackend-porting
  • compiler-performance-tuning
  • compiler-validation-reporting

这些 skill 的目标,是让之后的大模型不再从“写 kernel 模板”开始,而是默认按照编译器的方式工作。

目前,这套专门为编译器开发使用的 skill 也已经开源成为 Ninetoothed.skill 仓库https://github.com/InfiniTensor/ninetoothed.skill)。

综上,“不到10天,用大模型搓了一个编译器”这句话,其价值不在于开发速度,而在于验证了一个事实:大模型能够快速生成代码,但要让它写出一个像样的系统,人需要不断定义边界、纠正抽象层级、补足验证方法,并把这些经验沉淀成可复用的工作流。

这可能才是大模型辅助基础软件开发最有价值的地方。

🔥2026 夏季 InfiniTensor 训练营报名火热进行中!

欢迎访问 InfiniTensor 官网https://www.infinitensor.com )查看报名信息,关注公众号获取更多相关内容。

扫描下方二维码,加入训练营官方社群

关注与交流