跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 29 章编译期、后端与芯片33 分钟11 习题7 自测题

编译器工程:写 pass、测试与回归

把「读编译器」变成「改编译器」:C++ 与 Python pass 的双语结构、写一个新 pass 的完整流程、IR diff 与性能回归怎么测,以及评审时会被问什么。

本章进度状态 未读完习题 0 / 11自测 未作答

学习目标

读完本章,你应当能够:

  1. 拿起任意一个 TileLang pass,说出它在哪一侧(C++ 还是 Python)、依据是什么, 并复述「定义 → 实现 → 注册 → 挂载 → 测试」这条完整链路。
  2. 解释 tilelang/transform/_ffi_api.py 与 C++ 侧 TVM_FFI_STATIC_INIT_BLOCK() / refl::GlobalDef().def(...) 的对应关系,说清改名时哪三处必须同时改, 以及为什么改错的情形只在运行时暴露。
  3. 照着本章的两个最小示例写出一个能跑的新 pass:Python 版(不重编 TileLang)与 C++ 版(含 CreatePrimFuncPass 注册与挂载),并给出可复现的验证命令。
  4. 为一个 pass 设计四层测试(IR diff 基线 / 单元测试 / 边界条件 / 端到端对拍), 说出每层能证明什么、不能证明什么。
  5. 用 benchmark + pass_timing + lower_trace 判定一次改动是否造成性能回归, 并把回归定位到具体 pass。
  6. 说出昇腾侧的额外约束:两棵源码树各自改什么、适配器仓库 AGENTS.md 里哪些是 硬约束、昇腾侧的测试与基准入口在哪。

前置知识

  • 第 20 章「IR 与编译期管线精读」:本章是它的续集。第 20 章教你读出每个 pass 对 IR 做了什么,本章教你改出来。如果还分不清 IRModule / PrimFunc / SBlock / ForKind,先回去读第 20 章第 2 节。
  • 第 4 章「架构总览与一次编译的全流程」:知道五个阶段与 context.lower(mod) 这个 唯一分岔点。
  • 第 12 章「调试与性能分析」:lower_trace / pass_diff / compile_only 在那里 首次出现;本章只讲怎么用它们调试你自己写的 pass。
  • 第 21–23 章:布局推导、流水与同步、昇腾自动调度。那三章的 pass 是「读懂一个 pass 再改它」的最佳练习对象,但读本章不需要先读完它们。
  • 会读 Python 与 C++17;知道「pass 就是一个 IRModule → IRModule 的函数」。 不需要写过 CUDA,也不需要读过 TVM 源码。

本章回答一个问题:从「我能读懂这个 pass」到「我改了它,并且知道它没坏」, 中间缺的是哪几步。


1. 问题的由来

1.1 读 pass 和写 pass 差的不是知识,是流程

第 20 章结束时,你应该能看着 tilelang/ascend/pipeline.py 说出 80 条 pass 调用 各自在解决什么问题。但真正坐到键盘前要改一个 pass 时,冒出来的是另一批问题:

  • 我要加的这条检查,该写在 C++ 还是 Python?
  • 我写好了,怎么让它被 Python 看到?为什么 tilelang.transform.X() 找不到 我的函数?
  • 我把它挂进 pipeline 了,编译还是过的——怎么知道它到底跑了、跑对了?
  • 我把性能跑慢了 4%,是我这个 pass 干的吗?
  • 昇腾那边有一棵另外的仓库,我要改哪一个?

这五个问题都不是「API 怎么用」,而是工程流程问题。它们各自的答案都很短, 但缺一条就卡住。

1.2 一个真实的痛点:新检查有三种「静默失效」

考虑一个具体场景。你想加一条检查:「local 作用域的数组不能太大」。照着现有 verify pass 抄完之后,有三种失败方式是编译期完全不报错的:

  1. VisitStmt_(const BufferStoreNode *op) 少写一个下划线,成了 VisitStmt(const BufferStoreNode *op)——编译通过、pass 也跑,只是永远不会被 调用(C++ 的重载规则,见 3.3 小节)。
  2. C++ 注册名写成 "tl.transform.VerifyLocalAlloc",Python 侧调的是 _ffi_api.VerifyLocalAllocLimit()——AttributeError,但只在首次调用时炸。
  3. 检查逻辑全对,但它被挂在 LowerOpaqueBlock 之后,此时 alloc_buffers 已经 被改写、SBlock 也没了——它每次都「没发现问题」。

本章的每一小节,本质上都是在拆掉这三种静默失效中的一种。

1.3 本章的四条纪律

  1. 所有源码引用来自锁定版本。 行号一律写成 _vendor/tilelang-v0.1.15/src/transform/verify_buffer_init.cc:419-422 这种形式, 并且在 2026-10-01 现场核对过。涉及昇腾适配器的写 _vendor/tilelang-ascend-v0.1.2.000/。版本以 版本锁定 为准。
  2. 不写没有跑过的运行结果。 本机是 macOS(Apple Silicon),没有 NVIDIA GPU,没有昇腾 NPU,也没有安装 tilelang/tvm(import tilelang 直接 ModuleNotFoundError,command -v nvcc 无输出)。因此凡是「跑起来是什么样」 的结论,一律写成「应当是什么样」并给出验证命令;「能编译」这类结论一律标为 待验证。
  3. 测试不是附属品。 一个 pass 没有 IR 基线就不算完成:它没坏 与 它没改坏别的 是两件事。
  4. 一句话说不清的设计取舍,不要放进 pipeline。 加 pass 的成本不只是代码, 还有编译时间,以及每一个后续 pass 看到的 IR。

2. 概念与模型

双语结构:一条 pass 从 C++ 到 Python 的通路③ 注册④ 自动挂属性from .mod import X薄包装导出挂载(每条 pipeline 一行)C++ 入口(主体)src/transform/*.cc(42 个)+ src/ascend/transform/*.cc(31 个)Python 入口tilelang/transform/ 下 3 个 pass + tilelang/analysis/ 检查器③ 注册(只有 C++ 需要)TVM_FFI_STATIC_INIT_BLOCK + refl::GlobalDef().def("tl.transform.X", X)④ _ffi_api 自动生成属性tvm_ffi.init_ffi_api("tl.transform", __name__):注册名去掉前缀变成 _ffi_api.Xtilelang/transform/__init__.py薄包装 def X(): return _ffi_api.X();纯 Python pass 在这里直接 import对外唯一门面 tilelang.transform.*读者从 Python 侧看到的 pass 名字都在这里出现backend pipeline.pymod = tilelang.transform.X()(mod);CUDA 与昇腾各有一段
C++ 与 Python 两个入口最终都产出 tvm.transform.Pass:C++ 需要注册进 FFI,Python 直接 import,两者再由 _ffi_api 统一变成 tilelang.transform.*,供各后端 pipeline 挂载。
  • _ffi_api 不写逻辑:整个文件 6 行,只有一次 init_ffi_api;逻辑要么在 C++,要么在 tilelang/transform/ 下的 Python 模块里。
  • 注册名是唯一契约:C++ 的字符串 "tl.transform.X" 与 Python 的 _ffi_api.X 必须逐字对应,改名会让所有调用点在 import 期或首次调用期炸掉。
  • 挂载只有一行:没有注册表、没有插件机制,pipeline 就是一串 mod = pass()(mod),要改顺序就改这串调用的位置。
  • 昇腾适配器仓库用的是旧接口 tvm._ffi._init_api 与 TVM_REGISTER_GLOBAL,别把适配器里的 pass 代码直接拷进 v0.1.15。

2.1 一个 pass 的五个要素

不管写在哪一侧,一个 pass 都由五件事组成。本章后面所有内容都挂在这五个词上:

要素 问题 C++ 侧 Python 侧
定义 解决什么问题、精度边界在哪 文件头注释(verify_buffer_init.cc:1-16) 模块 docstring
实现 遍历哪些节点、怎么判定 StmtExprVisitor / StmtExprMutator 子类 PyStmtExprVisitor / PyStmtExprMutator + @tirx.functor.*
注册 Python 怎么找到它 TVM_FFI_STATIC_INIT_BLOCK() + refl::GlobalDef().def(...) 不需要,from .mod import X
挂载 什么时候跑 backend pipeline.py 里的 mod = X()(mod) 同一行,写在 tilelang/transform/__init__.py 导出后
测试 怎么证明它对了 testing/python/transform/test_*.py 同上

2.2 双语结构:哪些在 C++,哪些在 Python

图 1(图:fig-01-dual-tree-and-ffi)画了完整通路,这里只给判断规则。

由 C++ 实现(主体)

  • src/transform/*.cc:42 个顶层文件,另有 layout_inference/、common/ 两个子目录;通用 pass 都在这里(verify_buffer_init.cc、 pipeline_planning.cc、layout_inference/、thread_storage_sync.cc …)。
  • src/ascend/transform/*.cc:31 个顶层文件,另有 auto_schedule/ 子目录; 昇腾特有 pass 都在这里(insert_nd2nz.cc、insert_sync.cc、 materialize_multi_buffer.cc …)。

由 Python 实现

  • tilelang/transform/ 下 3 个:add_bufstore_wrapper.py、 decouple_type_cast.py、hoist_broadcast_values.py。它们的共同点是 改写形状清晰、不需要新 IR 节点、且仍在演进。
  • tilelang/analysis/ 下 5 个检查器:ast_printer.py、fragment_loop_checker.py、 layout_visual.py、nested_loop_checker.py、parallel_local_index_checker.py。
  • tilelang/ascend/analysis/vf_checker.py:VFChecker,被 tilelang/ascend/pipeline.py:95 直接调用。
  • tilelang/metal/transform/ 下 2 个。
  • 另有一类不是 pass 的 Python 函数:tilelang/ascend/transform/z3_scheduler.py 用 @tvm_ffi.register_global_func("tl.transform.z3_schedule_python")(:228)与 "tl.transform.z3_schedule_loop_python"(:784)注册了两个函数,由 C++ 侧 AutoSchedule 回调。它们不是 pass,是把 Python 当算法库用——这是昇腾路线 的一个特点,第 23 章详细讲。

怎么选? 按顺序问自己:

  1. 需要遍历/改写上游已有的 TIR 节点吗?需要 → 两边都能做,接着看第 2 问。
  2. 需要访问 C++ 侧独有的语义吗(TileOperator 接口、layout 对象、BufferRegion 上的 access 信息)?需要 → 写 C++。
  3. 改动会频繁迭代,或需要在没有编译环境的地方调试?→ 写 Python。
  4. 会被多个后端复用、且对编译器自身耗时敏感?→ 写 C++。

一个可检验的经验法则:verify_buffer_init 这类检查如果现在重写,很可能写成 Python——它只需要 BufferStore / BufferLoad 与 tile op 的读写集合;而 LayoutInference 必须留在 C++,因为它要访问 layout 对象与 op 的 InferLayout 接口。事实上 verify_parallel_loop.cc:3 就 #include "common/constr_visitor.h", 复用了 C++ 侧的约束集合基础设施(见第 23 章),这正是「留在 C++」的理由。

2.3 FFI:注册名怎样变成 Python 属性

这是最容易出错、也最容易讲清的一环。整条链路只有两行关键代码:

# tilelang/transform/_ffi_api.py:3,6(全文件 6 行,没有别的逻辑)
import tvm_ffi
tvm_ffi.init_ffi_api("tl.transform", __name__)
// src/transform/verify_buffer_init.cc:419-422
TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = reflection;
  refl::GlobalDef().def("tl.transform.VerifyBufferInit", VerifyBufferInit);
}

init_ffi_api("tl.transform", __name__) 做的是:把注册表中所有以 "tl.transform." 开头的全局函数,按去掉前缀后的名字,挂成本模块(__name__ 即 tilelang.transform._ffi_api)的属性。于是:

注册名 "tl.transform.VerifyBufferInit"
        └─────────┬──────────────┘
           前缀 tl.transform        ← 被 init_ffi_api 的第一个参数吃掉
                      余下 VerifyBufferInit → 变成属性 _ffi_api.VerifyBufferInit

改名时要同时改三处(code review 最常抓的一类错):

  1. C++ 里的注册字符串 "tl.transform.X";
  2. C++ 里 CreatePrimFuncPass(pass_func, 0, "tl.X", {}) 的第三个参数——它出现在 TL_LOWER_TRACE / TL_PASS_PROFILE 报告里,可以和注册名不同;
  3. Python 里的薄包装 def X(): return _ffi_api.X()。

第 1、3 处改错 → AttributeError;第 2 处改错 → 不报错,只是报告里出现一个你 认不出来的名字。三种错误的暴露时机不同,这就是 3.2 节说「必须有一条会命中 的测试」是唯一可靠防线的原因。

2.4 挂载:命令式 pipeline,没有注册表

熟悉 LLVM 或 TVM 官方 PassManager 的人会本能地去找「注册到某个 pass manager」。 TileLang 不是这样。挂载就是一行函数调用:

# tilelang/cuda/pipeline.py:97-98
mod = tilelang.transform.VerifyBufferInit()(mod)

每条 pipeline 就是一个普通 Python 函数(CUDAPassPipelineBodyPrologue / CUDAPassPipelineBody / AscendPassPipelineBody),由 PassPipeline 包一层 instrumentation 作用域(tilelang/backend/pass_pipeline/pipeline.py:11,22-31):

CUDA_PIPELINE = PassPipeline("cuda", CUDAPassPipelineBody)         # pipeline.py:284
ascend_pipeline = PassPipeline("ascend", AscendPassPipelineBody)   # ascend/pipeline.py:190

这个设计有两个直接后果,写 pass 时都要记住:

  • 顺序是硬编码的,没有「优先级」可以声明。你的 pass 放在哪一行,就在哪个阶段 的 IR 形态上工作。
  • 想被所有后端复用,就得在每个后端各加一行。 一个通用 pass (如 VerifyBufferInit)在 v0.1.15 里被 6 条 pipeline 各自调用一次: cuda/pipeline.py:98、ascend/pipeline.py:76、cpu/pipeline.py:35、 metal/pipeline.py:40、rocm/pipeline.py:39、webgpu/pipeline.py:41。 DecoupleTypeCast 同样是 6 处。「只挂了一个后端」是 PR 评审里最常见的遗漏。

2.5 pass 之间的顺序约束

把 pass 加进 pipeline 时,真正要判断的是它需要的 IR 形态在什么时候还在。 锁定版本里现成的例子:

  • VerifyBufferInit 必须在 PipelinePlanning 之前。CUDA 侧的注释写得很直白 (tilelang/cuda/pipeline.py:93-98):「Runs after the reducer passes above … and before PipelinePlanning and LowerTileOp, while loop bodies are still in source order and tile ops still declare their access regions.」——它依赖 「源码顺序」和「tile op 的 GetAccessRegions() 还在」两个前提。
  • DecoupleTypeCast 必须在 VectorizeLoop 之前,理由写在 tilelang/transform/decouple_type_cast.py:650-652:「must be applied before VectorizeLoop and StorageRewrite passes, while the IR still uses BufferLoad/BufferStore (not tvm_access_ptr)」。
  • AddWrapperForSingleBufStore 挂在几乎所有 pipeline 的最前面 (cuda/pipeline.py:79、ascend/pipeline.py:58),因为它要把「裸 store」包成 For(PARALLEL),而之后所有 pass 都假设 store 已经在并行循环里。

一张可复用的判断表:

你的 pass 需要什么 它必须放在谁之前
T.copy / T.gemm 这类 tile op 还在,且带 access region LowerTileOp 之前
循环体还是源码顺序、for 还没被打散 PipelinePlanning / InjectSoftwarePipeline 之前
还是 BufferLoad / BufferStore,没被降成 tvm_access_ptr VectorizeLoop / FlattenBuffer 之前
SBlock 还在,alloc_buffers / annotations 还能读 LowerOpaqueBlock 之前
thread_extent 已经是真实线程域(只对 SIMT 后端) MaterializeKernelLaunch 之后
已有 layout 信息 LayoutInference 之后

3. 用法:从零写一个 pass

一个 C++ pass 的骨架解剖1. ① 文件头注释(L1-16)写清三件事:pass 干什么、为什么需要、精度边界(deliberately imprecise in one direction)2. ② includes(L18-37)../op/*.h 复用 tile op 语义;span_utils.h 行号渲染;<tvm/tirx/stmt_functor.h>;<tvm/runtime/logging.h>3. ③ namespace + 别名(L39-44)namespace tvm::tl { using namespace tirx; using namespace ffi; }——using namespace ffi 才能写 refl::GlobalDef4. ④ 算法:StmtExprVisitor / Mutator(L46-396)PotentialWriteCollector 收集谁可能写,BufferInitVerifier 做判定;reports_ 与 EmitReport() 合成一条 LOG(WARNING)5. ⑤ pass 工厂(L400-417)CreatePrimFuncPass(pass_func, 0, "tl.VerifyBufferInit", {});只报不改的 pass 在这里原样 return f6. ⑥ FFI 注册(L419-422)TVM_FFI_STATIC_INIT_BLOCK + refl::GlobalDef().def("tl.transform.VerifyBufferInit", VerifyBufferInit)
以 src/transform/verify_buffer_init.cc(426 行)为例,一个 C++ pass 的骨架是六块;作为对照,Python pass 只用四块,没有 ⑥ FFI 注册。
  • 静默失败一:VisitStmt_(const BufferStoreNode *op) 少写一个下划线会编译通过、pass 也跑,但重载永远不会被调用——唯一防线是一条必须命中的测试。
  • 静默失败二:注册名与 Python 调用不一致只在运行时暴露,报错形态是 AttributeError: module ... has no attribute 'X'。
  • Python pass 的骨架只有四块:imports → pass 工厂 → 闭包内的 pass_fn / @tirx.functor.mutator → return prim_func_pass(...),它不需要 FFI 注册。

3.1 五步法

① 定义     写清「报什么、不报什么、精度边界在哪」。这段会进文件头注释。
② 选语言   按 2.2 节的四问决定写 C++ 还是 Python。
③ 实现     写出 visitor / mutator;pass_func 保持「要么原样返回,要么返回新 func」。
④ 注册      C++:注册宏 + Python 薄包装;Python:写进 tilelang/transform/__init__.py。
⑤ 挂载+测试 对着 2.5 节的表选位置挂进 pipeline,再补四层测试。

下面给两个最小完整示例。Python 版可以立刻验证语法与 API; C++ 版需要编译环境,本机没有验证过。

3.2 示例 A:Python pass(不重编 TileLang)

完整代码在 code/01-python-pass/:hoist_constant_broadcast.py 是 pass 本体, test_hoist_constant_broadcast.py 是三个 IR 前后对照用例。

它做的事是:把 buffer store 里的立即数广播 T.Broadcast(<IntImm|FloatImm>, lanes) 提取成一个 Bind 定义的变量。原型是真实存在的 tilelang/transform/hoist_broadcast_values.py(pass 主体在 :75-99,mutator 在 :16-72);我们把它简化、改名,保留两个关键设计点:

@tirx.functor.mutator
class HoistConstantBroadcastMutator(PyStmtExprMutator):
    def __init__(self) -> None:
        super().__init__()
        self.pending: list[tuple[Var, tirx.PrimExpr]] = []

    def visit_broadcast_(self, op: Broadcast):
        if isinstance(op.value, (tirx.IntImm, tirx.FloatImm)):
            value = self.visit_expr(op.value)
            new_var = Var("broadcast_value", dtype=value.dtype)
            self.pending.append((new_var, value))
            return Broadcast(new_var, op.lanes)
        return Broadcast(self.visit_expr(op.value), self.visit_expr(op.lanes))

    def visit_buffer_store_(self, op: BufferStore):
        saved_pending = self.pending
        self.pending = []                      # ① 进入子语句:清空
        new_indices = [self.visit_expr(index) for index in op.indices]
        new_stmt = BufferStore(op.buffer, self.visit_expr(op.value), new_indices)
        if self.pending:                       # ② 离开子语句:把定义挂到语句之前
            stmts: list[tirx.Stmt] = [Bind(var, value) for var, value in self.pending]
            stmts.append(new_stmt)
            new_stmt = SeqStmt(stmts)
        self.pending = saved_pending           # ③ 恢复
        stamp_stmt_spans(new_stmt, get_stmt_span(op))
        return new_stmt


def HoistConstantBroadcast():
    def pass_fn(func: PrimFunc, mod, ctx) -> PrimFunc:
        mutator = HoistConstantBroadcastMutator()
        return func.with_body(mutator.visit_stmt(func.body), span=func.span)

    return prim_func_pass(pass_fn, opt_level=0, name="tl.HoistConstantBroadcast")

四个逐行要点:

  • PyStmtExprMutator + @tirx.functor.mutator:装饰器把 Python 方法 visit_broadcast_ 接到 C++ visitor 的 VisitExpr_(const BroadcastNode*) 上, 命名规则是「visit_ + 小写节点名 + _」。写错名字不报错,只是不会被调用。
  • pending 的清空—恢复:这是 mutator 的局部状态管理。少了这三行,第二条 store 会引用第一条 store 定义的变量,生成的 IR 直接非法。
  • stamp_stmt_spans:重建节点会丢 span,而 span 是编译报错指回 Python 行的 唯一依据(TL_EMIT_LINE_DIRECTIVES 也依赖它)。tilelang/ir.py 里的 get_stmt_span(:105)与 stamp_stmt_spans(:123)就是为此存在。
  • prim_func_pass(pass_fn, opt_level=0, name=...):name 决定 TL_LOWER_TRACE / TL_PASS_PROFILE 报告里的名字。不传 name 的 Python pass (如上游 HoistBroadcastValues,hoist_broadcast_values.py:99)在报告里会退化 成自动推断的名字——能跑,但不好认。

挂载只有两处改动:

# tilelang/transform/__init__.py,紧挨其他 Python pass 的 import
from .hoist_constant_broadcast import HoistConstantBroadcast  # noqa: F401

# 某个 backend 的 pipeline 里(例如 tilelang/cuda/pipeline.py)
mod = tilelang.transform.HoistConstantBroadcast()(mod)

验证级别:本机只做到 python3 -m py_compile 通过,以及文件里出现的每个 API 名逐个在 _vendor/tilelang-v0.1.15/ 里核对。真正跑测试需要装好 tilelang (不需要 GPU,因为用例停在 IRModule 层)。

3.3 示例 B:C++ pass(模板 + 构建步骤)

完整代码在 code/02-cpp-pass-template/:count_parallel_loops.cc 是 pass 本体, python_wrapper_snippet.py 是三处改动的粘贴片段, test_count_parallel_loops.py 是 pytest 骨架。它同样「只报不改」,数出所有 For(kind=kParallel) 并打成一条警告。骨架逐块对照上游:

模板里的部分 对照的上游代码(_vendor/tilelang-v0.1.15/)
struct ... : public StmtExprVisitor + VisitStmt_(const ForNode*) src/transform/verify_parallel_loop.cc:28-56
SpanHintSuffix({span}) + #include "span_utils.h" src/transform/verify_buffer_init.cc:22,383
CreatePrimFuncPass(pass_func, 0, "tl.X", {}) src/transform/verify_buffer_init.cc:416
TVM_FFI_STATIC_INIT_BLOCK + refl::GlobalDef().def(...) src/transform/verify_buffer_init.cc:419-422、src/ascend/transform/insert_nd2nz.cc:488-490
Python 薄包装 tilelang/transform/__init__.py:86-94

核心三块(完整版见文件):

namespace tvm::tl {
using namespace tirx;          // StmtExprVisitor / ForKind / PrimFunc
using namespace ffi;           // 让下面的 refl::GlobalDef 能解析

namespace {
struct ParallelLoopReporter : public StmtExprVisitor {
  std::vector<LoopInfo> loops_;
  void VisitStmt_(const ForNode *op) final {
    if (op->kind == ForKind::kParallel) {
      loops_.push_back({op->span, StaticExtent(op->extent)});
    }
    StmtExprVisitor::VisitStmt_(op);
  }
  void EmitReport() const { /* 合成一条 LOG(WARNING) */ }
};
} // namespace

using namespace tirx::transform;

tvm::transform::Pass CountParallelLoops() {
  auto pass_func = [=](PrimFunc f, IRModule m, PassContext ctx) {
    ParallelLoopReporter reporter;
    reporter(f->body);
    reporter.EmitReport();
    return f;                          // 只报不改
  };
  return CreatePrimFuncPass(pass_func, 0, "tl.CountParallelLoops", {});
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = reflection;
  refl::GlobalDef().def("tl.transform.CountParallelLoops", CountParallelLoops);
}
} // namespace tvm::tl

必须知道的三个 C++ 陷阱(都写进了文件注释):

  • 重载名不能写错。VisitStmt_(const ForNode *op) final 写成 VisitStmt(op) 会 编译通过、静默不生效。这是 C++ pass 最常见的一类「什么都没发生」。
  • pass_func 里先记录还是先递归必须和你的语义一致:对报告型 pass 只是排序 问题,对改 IR 的 pass 就是语义错误。
  • CreatePrimFuncPass(pass_func, 0, ...) 的 opt_level=0 意味着「必须执行」, 不会因优化开关被跳过。语义 pass(含检查)都应该用 0。

构建与验证步骤(本机没有编译环境,以下命令是待验证项,完整版见 资料库索引):

# 准备一棵可写的 checkout(不是 _vendor/)
git clone --recurse-submodules git@github.com:<your-name>/tilelang.git
cd tilelang
uv venv --seed .venv && source .venv/bin/activate
uv pip install --requirements requirements-dev.txt
pre-commit install --install-hooks

# 落地:新 .cc + Python 薄包装 + pipeline 一行 + 测试
cp <THIS_DIR>/count_parallel_loops.cc src/transform/count_parallel_loops.cc
cp <THIS_DIR>/test_count_parallel_loops.py testing/python/transform/

# 编译扩展
python3 -m pip install --no-build-isolation --verbose --editable .

# 冒烟:确认 Python 能拿到这个 pass(不需要 GPU)
python3 -c "import tilelang; print(tilelang.transform.CountParallelLoops())"

# 单元测试(前两条不需要 GPU)
python3 -m pytest testing/python/transform/test_count_parallel_loops.py -q

# 风格
bash format.sh --files src/transform/count_parallel_loops.cc

为什么第 6 步要重新 configure:CMake 用 file(GLOB ...) 收集源文件 (CMakeLists.txt:374-389),glob 在 configure 期求值。新加 .cc 后不重新 configure,文件不会进构建,于是冒烟那一步报的仍然是 AttributeError——很容易 误判成「注册名写错了」。

3.4 给 pass 加开关

如果检查可能误报,就给它一个开关。仓库里现成的模式是 tl.disable_buffer_init_check:

// src/op/builtin.h:118-124(注释 + 常量名)与 src/op/builtin.cc:37(注册)
static constexpr const char *kDisableBufferInitCheck =
    "tl.disable_buffer_init_check";
TVM_REGISTER_PASS_CONFIG_OPTION(kDisableBufferInitCheck, Bool);
// src/transform/verify_buffer_init.cc:406-408
if (ctx->GetConfig(kDisableBufferInitCheck, Bool(false)).value()->value) {
  return f;
}

Python 侧的枚举在 tilelang/transform/pass_config.py:52-58 (TL_DISABLE_BUFFER_INIT_CHECK);pipeline 里的源码级守卫用 tilelang/backend/pass_pipeline/pipeline_utils.py 里的一组 should_* / allow_* 函数(:25-76),例如 should_enable_race_check() 决定 VerifyParallelLoop 挂不挂(cuda/pipeline.py:83-84)。

加一个 config key 要改四处:src/op/builtin.h 的常量、src/op/builtin.cc 的 TVM_REGISTER_PASS_CONFIG_OPTION、C++ 里的 ctx->GetConfig(...)、 tilelang/transform/pass_config.py 的枚举。少一处,用户就会拿到「配置不生效但 也不报错」。


4. 实现机制

4.1 三条职责链

职责 位置 关键点
序列 tilelang/{cuda,ascend,cpu,metal,rocm,webgpu}/pipeline.py 纯 Python,一行一个 pass;PassPipeline(tilelang/backend/pass_pipeline/pipeline.py:11)只包 instrumentation 作用域与错误富化
逻辑 src/transform/*.cc、src/ascend/transform/*.cc、tilelang/transform/*.py、tilelang/analysis/*.py 编译器算法本身
门面 tilelang/transform/__init__.py + _ffi_api.py 对外的唯一入口;_ffi_api.py 只有 6 行

tilelang/transform/__init__.py 是唯一门面,读者从 Python 侧看到的 pass 名字 都在这里出现。它同时 import 两类东西:C++ 包装( def PipelinePlanning(): return _ffi_api.PipelinePlanning(),:19-26)与纯 Python pass(from .hoist_broadcast_values import HoistBroadcastValues,:10)。

4.2 pass 名是怎么被观察工具看到的

TL_LOWER_TRACE / TL_PASS_PROFILE / TILELANG_PASS_DIFF 是同一个机制的 不同消费者,机制在 tilelang/instrumentation/:

  • tilelang/instrumentation/session.py:163 的 compile_pass_instrumentation(...) 开一个「一次编译」的作用域;
  • :213 的 instrument_current_pass_context() 把已注册的工具装进 TVM 的 PassContext;
  • 工具通过 :130 的 register_pass_instrumentation_tool(...) 注册自己;
  • PassPipeline.lower() 用 pass_pipeline(self.name) 给每个 pass 事件打阶段标签 (pipeline.py:22-31)。

于是两个工具都只是「注册一个工具」:

  • tilelang/tools/pass_timing.py:334 的 create_pass_timing_tool(pass_configs), 由 tilelang/jit/kernel.py:243 在每次 JIT 编译时装配;
  • tilelang/tools/lower_trace/__init__.py 的 lower_trace(func, passes, ...) (:49)与 tilelang/tools/lower_trace/core.py:663 的 enable()。

对你写 pass 的直接意义:只要给 CreatePrimFuncPass / prim_func_pass 传一个 可读的 name,你的 pass 就会带着这个名字出现在这三份报告里。

4.3 instrumentation 与「pass 有没有跑」的关系

新手最容易困惑的一点:TL_LOWER_TRACE=1 看不到你的 pass,不代表它没跑。 lower_trace 挂在完整编译入口(tilelang/engine/lower.py:117,162,234 三处 compile_pass_instrumentation)上;如果你只是单独调 tilelang.transform.X()(mod),那就没有 session,自然也没有记录。 想观察单个 pass,用程序 API:

from tilelang.tools.lower_trace import lower_trace
import tilelang

lower_trace(func, [("Before", tilelang.transform.Simplify()),
                   ("Mine",   tilelang.transform.HoistConstantBroadcast())],
            mode="terminal")

4.4 新增 C++ 文件要动的构建点

结论已经说过一次,这里给出完整清单,因为它是「编译通过但不生效」的最常见根因:

新文件类型 需要动的地方 为什么
src/transform/*.cc 无需改 CMake,但要重新 configure CMakeLists.txt:374-389 的 file(GLOB ...) 在 configure 期求值
src/ascend/transform/*.cc 同上;且要 USE_ASCEND=ON src/ascend/CMakeLists.txt:15-20 只无条件收 3 个 always 文件,:22 处 if(NOT USE_ASCEND) return(),transform/*.cc 在 :38 才进列表
新的 Python 模块 tilelang/transform/__init__.py 加 import Python 侧没有自动发现机制

昇腾这一点尤其容易踩:src/ascend/CMakeLists.txt:1-20 的注释解释了为什么 src/ascend/ir.cc、src/ascend/layout/ascend_layouts.cc 等必须无条件编译—— tilelang/__init__.py 会无条件 import tilelang.ascend,缺了对象类型注册就会 Cannot find object type index,直接 import tilelang 失败。而 transform/ 下的 pass 只在 USE_ASCEND=ON 时进构建。


5. pass 的正确性测试

一个 pass 的四层测试:每层能证明什么① 端到端对拍② 边界条件③ 单元测试④ IR diff 基线证明什么整条 pipeline + codegen + 运行时都活着边界不被吞;假警报不会把检查关掉pass 在给定 IR 上确实做了你预期的事这次改动的 IR 变化是你预期的不能证明什么性能没退(见图 4)大规模输入下的行为没被别的 pass 之后覆盖变化是对的;性能没退现成范式test_...verify_buffer_init.py:447-483同文件 :106-313 的一批反向用例test_...verify_parallel_loop.py:23-43test_...hoist_broadcast_values.py:56-64关键写法@tilelang.testing.requires_cuda + tilelang.compile(...)extern 填充、global scope、参数 buffer、读后再写……手搓 IRModule → 调 pass → 断言 stderr 文本before/after 各写成 @T.prim_func,BindTarget 对齐后 assert_structural_equal
四层不是可选项,而是四种不同的证据:便宜、快、离线 ← 从左到右 → 昂贵、慢、要硬件;缺哪一层,就有一类错误永远逃得掉。
  • ④ 和 ③ 的差别比看起来大:④ 断言结构相等(最严格,也最容易因无关的命名/顺序变化变红),③ 断言行为(对文本格式敏感,但对实现细节宽容)。
  • 两者都写,是因为一种 bug 只有一种断言抓得住:「把不该改的地方改了」只有 ④ 能抓,「该报的没报」只有 ③ 能抓。
  • 基线要由脚本生成,不要手写:手写 after 只在用例极小时可行,超过 30 行就该用快照在 CI 里 diff。

5.1 四种证据,缺一不可

见图 3(图:fig-03-test-layers)。下面给每层一个最小可用的做法。

5.2 IR diff 基线

最严格的一层:把 pass 作用前后的 IR 都写下来,断言结构相等。

def _check(before, after) -> None:
    mod = tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
    mod = tvm.tirx.transform.BindTarget(AUTO_TARGET)(mod)
    mod = HoistConstantBroadcast()(mod)

    expected = tvm.IRModule.from_expr(after.with_attr("global_symbol", "main"))
    expected = tvm.tirx.transform.BindTarget(AUTO_TARGET)(expected)
    tvm.ir.assert_structural_equal(mod["main"], expected["main"], map_free_vars=True)

三个细节:

  • BindTarget 不能省。 没 bind target 的 IR 与 bind 过的 IR 结构不等, 不 bind 会让「本来一样」的两次比较挂掉。
  • map_free_vars=True。 用例里的 Var 是自由变量,允许按位置映射比较。
  • 基线要能一键生成。 手写 after 只在用例极小时可行;一旦超过 30 行, 就该用脚本把当前输出写成快照并在 CI 里 diff。

现成范式:testing/python/transform/test_tilelang_transform_hoist_broadcast_values.py:56-64 (_check)与 :67-133(四个 before/after 用例,其中一个专门守一个历史 bug)。

5.3 单元测试:testing/ 的写法

目录约定:

testing/python/transform/test_tilelang_transform_<pass 名小写>.py   # 通用 pass
testing/ascend/transform/test_ascend_<pass 名小写>.py               # 昇腾 pass
testing/ascend/_ir.py                                               # 昇腾侧共用 IR 构造助手

昇腾侧特别注意 testing/ascend/_ir.py:它提供 seq() / region() / copy() / kernel() / calls() / nodes() 这组助手(:15-58),让你能手搓一个带 tl.tileop.ascend_copy 的 IR,而不用起一个前端 kernel——因为昇腾 pass 的输入常常 是「已经排过序的 IR」。用法见 testing/ascend/transform/test_ascend_insert_nd2nz.py:11-46。

通用 pass 侧更朴素:手搓 IRModule → 调 pass → 断言。两个范式:

# 断言 stderr 文本(verify / report 型 pass)
def _verify(mod, capfd) -> str:
    tilelang.transform.VerifyBufferInit()(mod)
    return capfd.readouterr().err

assert MESSAGE in _verify(kernel, capfd)
# 断言 IR 里出现了某个 op(重写型 pass)
from testing.ascend._ir import calls
(scatter,) = calls(after, "tl.ascend_nd2nz_scatter")
assert not calls(after, "tl.tileop.ascend_copy")

5.4 端到端对拍

前三层都在 IR 层。真正会漏掉的是:新 pass 让真实编译挂掉。所以至少要有一条:

@tilelang.testing.requires_cuda
def test_pipeline_warns_by_default(capfd, no_kernel_cache):
    tilelang.compile(_gemm_kernel("none"))
    assert MESSAGE in capfd.readouterr().err

两个来自上游的细节值得照抄:

  • no_kernel_cache fixture(test_tilelang_transform_verify_buffer_init.py:377-388): 命中 kernel cache 时根本不跑 pass,警告会「第二次编译时消失」,用这个 fixture 强制每次重编。
  • 数值对拍要和 torch.allclose 一起写:编译过只是活着,算对才是正确。 昇腾侧对应的是 torch_npu 张量与 T.dump_tensor(见第 19 章)。

5.5 边界条件清单

verify pass 的价值几乎全在边界上。以 verify_buffer_init.cc 的用例集为模板, 一个检查至少要有这几类反向用例(每一条都对应上游一个真实测试):

边界 为什么必须有 上游用例
恰好不在管辖范围(global scope) 防止过度报告 test_tilelang_transform_verify_buffer_init.py:111-120
由外部手段建立(call_extern 填的 buffer) 保守策略「当作已写」 同文件 :106-108
读自己的目的地址(x[i] = x[i] + 1) store 先求值再写入 同文件 :183-201
跨线程作用域的顺序无关性(warp specialization) shared 靠 barrier 而不是源码顺序 同文件 :218-237
参数 buffer(由调用方填) 不该报 同文件 :123-141
动态形状 / 折不动 折不动就不报 见 code/03-verify-pass-template/
一个 buffer 读多次只报一次 报告可读性 同文件 :358-371

6. 性能回归

判定性能回归并定位到 pass 的流程1. ① 判定:一次对比算不算回归maint/scripts/run_perf_regression.sh 造 current 与基线两个 venv + wheel;regressed ⟺ 至少 2 条 speedup < 0.902. ② 确认不是噪声:重跑REFRESH=1 bash maint/scripts/run_perf_regression.sh -k <算子/用例>;重跑后仍低于阈值才继续往下3. ③ 定位「编译期变慢」还是「kernel 变慢」TILELANG_PASS_PROFILE=1 python3 <用例>.py → tilelang/tools/pass_timing.py 的 inclusive / self 用时4. ④ 看「哪些 pass 改了 IR」TL_LOWER_TRACE=terminal python3 <用例>.py → tilelang/tools/lower_trace/ 逐 pass before/after diff(TILELANG_PASS_DIFF 已被取代)5. ⑤ 定位到具体 pass回到图 3 的四层测试,为这次的改动补一条 IR 基线
每一步都对应一个可执行的命令或一个已存在的文件:benchmark 判定 → 重跑排除噪声 → pass_timing 分编译期/产物 → lower_trace 找 diff → 回到四层测试补基线。
  • 「编译慢」和「跑得慢」是两件事:pass_timing 量前者,benchmark 量后者;一个 pass 让编译慢 3 倍但产物一样时,kernel 性能不变。
  • pass_timing 给 inclusive 与 self 两个数,self 才是这个 pass 自己花掉的;看到外层 pass 很慢先看 self。
  • 回归判定需要样本量:单条掉到 0.044x 也不算回归,只有 ≥2 条同时掉破阈值才算。

6.1 benchmark 方法

三个层次,按需选:

层次 入口 说明
单个算子的端到端对比 _vendor/tilelang-v0.1.15/benchmark/(matmul/、matmul_fp8/、mamba2/、blocksparse_attention/、compile_speed/、matmul_metal/) 回答「这个 kernel 快不快」
公共算子集 corpus tilelang-benchmark/:资料库索引 给出 GEMM / FlashAttention / Linear Attention 三张形状表,hopper_benchmark/、ampere_benchmark/、ada_benchmark/、cdna_benchmark/ 分区存放各代硬件的结果 回答「同一批形状跨硬件横向对比」;注意该仓库标题已写明 Archived、作者指向 TileOPs
改动前后的回归判定 maint/scripts/run_perf_regression.sh 回答「我的 PR 有没有把谁改慢」——这才是改 pass 时要跑的

run_perf_regression.sh 的做法值得记住:它不在你的工作树里切分支,而是 git worktree add --detach 出一份基线 checkout(:150-168),两边分别建 venv、 建 wheel(带缓存,:240-369),各跑一遍 run_current_regression.py 产出 JSON, 最后交给 compare_perf_regression.py 出 md / png / json。常用环境变量写在脚本头 :15-32:

# 只跑你改动相关的那几组
./maint/scripts/run_perf_regression.sh -k gemm/regression_example_gemm.py

# 判为回归就退出码非零,供 CI 用
FAIL_ON_REGRESSION=1 REGRESSION_THRESHOLD=0.95 ./maint/scripts/run_perf_regression.sh

6.2 怎么判断「是不是回归」

不要看单条数字。 仓库里的判定规则在 maint/scripts/test_perf_regression.py:24-32,243-257:

speedup = old_latency / new_latency
regressed ⟺ 至少有 MIN_COUNT 条(默认 2)speedup < THRESHOLD(默认 0.90)

那段注释给出了标定依据:到 PR #3176 为止的 33 份机器人报告、1901 个样本中, 真回归都表现为 14–23 条同时塌陷;而 7 份假警报恰好只有一条落在极端值 (最低 0.044x)上,其余全是 1.000。于是「至少两条同时掉」这个门槛正好把两类分开。

code/04-regression-judge/ 把这条规则抽成可离线运行的判定器,--self-test 输出 (本机真实运行过):

场景 A:40 条里 1 条 0.85x
**不判为回归**:1 条低于 0.9x,未达到 2 条的门槛。

场景 B:40 条里 2 条 0.88x
**回归:2/40 条低于 0.9x**

也就是说:「有一条变慢了」和「发生了回归」是两句不同的话,后者需要样本量。 同一个脚本还演示了第三种情况——只在一边出现的 case 不参与比较, 「少了结果」与「结果变差」在 compare_perf_regression.py 里对应 --fail-on-missing 与 --fail-on-regression 两个不同开关。

注意两个不同的默认值,别把它们混成一个。 上面这套「0.90 + 至少 2 条」是 PR 机器人报告的判定口径(maint/scripts/test_perf_regression.py:31-32)。而 run_perf_regression.sh:28 给 FAIL_ON_REGRESSION 用的 REGRESSION_THRESHOLD 默认是 1.0,也就是「只要有一条比基线慢就退出码非零」——这是 CI 门禁的保守 选择,不是回归判定本身。要做批量判定,用前者;要做门禁,用后者并显式设定阈值。

6.3 定位回归来源

判定为回归之后,第一步是分清编译变慢还是产物变慢——两件独立的事, 两套工具:

# ① 编译期:每个 pass 的 inclusive / self 用时
TILELANG_PASS_PROFILE=1 python3 my_case.py
#    入口:tilelang/tools/pass_timing.py:334 create_pass_timing_tool

# ② 产物:哪些 pass 改了 IR
TL_LOWER_TRACE=terminal python3 my_case.py
#    入口:tilelang/tools/lower_trace/__init__.py:49 / core.py:663

读 pass_timing 的关键是分清两列:self 是这个 pass 自己花的时间,inclusive 包含它调用的子 pass(tilelang/tools/pass_timing.py:64-73 的 PassTimingRecord.self_duration_s / duration_s)。看到「外层 pass 很慢」先看 self,否则会把子 pass 的成本算到外层头上。

读 lower_trace 的关键是只盯你自己那个 pass 的 diff:报告按阶段与顺序编号, 你要确认三件事——它确实被调用了、它的 before/after 差异是你预期的那几行、 它之后的下游 pass 没有因此产生额外变化。

图 4(图:fig-04-regression-triage)把这一步画成了流程。

6.4 昇腾侧的回归入口

昇腾不共用 CUDA 的脚本:

  • maint/scripts/run_perf_regression_ascend.sh:基线默认取 https://github.com/tile-ai/tilelang.git 的 main,用 NINJA_JOBS 控制并行重编(默认 64),可用 SKIP_BUILD=1 跳过两次 checkout 之间的重编。
  • maint/scripts/ascend_perf_regression.py:对应的 Python 侧驱动(由 run_perf_regression_ascend.sh:55 的 DRIVER_PATH 调用)。
  • maint/scripts/run_local_ci_test_ascend.sh:单测入口,跑 examples/ascend/ 与 testing/ascend/,不加载 CUDA 调度插件、不探测 GPU,默认 4 个 example worker + 32 个单测 worker。

7. 调试一个 pass 的手段

7.1 IR 打印

按成本从低到高:

  1. mod.script():把 IRModule 打印成 TIR 文本。写单测时最常用, 加减一行就能看。
  2. TL_LOWER_TRACE=1:跑一次真实编译,拿逐 pass 的 HTML 报告 + 磁盘上的 .tir 快照(官方说明见 docs/tools/lower_trace.md)。这是观察别人的 pass 最省事的方式。
  3. TL_ENABLE_DUMP_IR / TL_DUMP_IR_PATH:常量定义在 src/op/builtin.h:129-130,适合在 CI 或崩溃现场留档。
  4. 设备侧打印(T.printf / T.dump_tensor):只能回答「运行时值对不对」, 回答不了「IR 变成什么样」。第 12 章讲过。

7.2 pass_diff 与 lower_trace

两者都是「逐 pass diff」,差别在工程成熟度:

  • tilelang/utils/pass_diff.py:1030 的 pass_diff(func, passes, mode=...) 是程序 API:给定 pass 链,输出终端彩色 diff 或 HTML。适合「我就想看我这一小段」。
  • TL_LOWER_TRACE / tilelang/tools/lower_trace/ 是整条 pipeline 的观察器: 带阶段标签、codegen 捕获、可编辑重编、崩溃安全的增量 HTML。

官方文档已经把 pass_diff 标成被取代:docs/tools/pass_diff.md 顶部有一条 admonition「Superseded — use IR Lower Trace」, TILELANG_PASS_DIFF 仅为向后兼容保留。新写 pass 时用 lower_trace。

7.3 断言:三种强度

手段 什么时候用 崩溃形态
ICHECK(cond) << msg 内部不变量(「这里必须有 tilelang_root」) 编译期立即失败,带上下文
LOG(FATAL) << msg 用户代码导致的不可能状态(如管线规划冲突) 编译期立即失败
LOG(WARNING) << msg 可绕过的问题(未初始化 buffer、数据竞争) 编译继续,警告进 stderr

经验法则:用户能修的问题用 WARNING,编译器自己的 bug 用 ICHECK。 把「用户写错了」做成 FATAL,会让 tilelang.compile 直接抛异常;反过来把内部 不变量做成 WARNING,会让 bug 潜伏到很后面才以别的方式爆出来。

顺便注意 WARNING 的可见性:tilelang/transform/pass_config.py:60-68 说明 tl.disable_data_race_check 默认是关的(因为易误报),需要 TILELANG_ENABLE_DATA_RACE_CHECK 环境变量或 pass config 打开。写新检查时, 「默认开还是默认关」本身就是一个要写进 docstring 的决定。

7.4 用 verify pass 当模板

verify_buffer_init 与 verify_parallel_loop 是最好的入门模板,因为它们 不改 IR,把「写 pass」的风险压到只剩遍历一种。 code/03-verify-pass-template/extract_verify_skeleton.py 可以把这两份骨架连行号 打印出来(本机已运行)。它的输出长这样:

=== verify_buffer_init ===
  header comment       : lines 1-16 (16 lines)
  total lines          : 426
  visitor overrides:
    L 196  void VisitStmt_(const BufferStoreNode *op) final {
    L 201  void VisitExpr_(const CallNode *op) final {
    L 296  void VisitStmt_(const EvaluateNode *op) final {
    L 315  void VisitStmt_(const BufferStoreNode *op) final {
    L 323  void VisitExpr_(const BufferLoadNode *op) final {
    L 330  void VisitExpr_(const CallNode *op) final {
  EmitReport           : L372
  pass factory         : L400  Pass VerifyBufferInit()
  CreatePrimFuncPass   : L416  CreatePrimFuncPass(pass_func, 0, "tl.VerifyBufferInit", {})
  FFI block            : lines 419-422
    registered name    : tl.transform.VerifyBufferInit

(看到两个同名的 VisitStmt_(BufferStoreNode*) 不要去重:文件里有两个 visitor—— PotentialWriteCollector 与 BufferInitVerifier,一个收集「谁可能写」, 一个做判定。这是这类 pass 的标准分工。)

抄这三块就够了:

  1. pass_func 永远原样返回 f(verify_buffer_init.cc:400-417);
  2. reports_ + EmitReport() 把发现合成一条警告 (verify_parallel_loop.cc:125-148);
  3. 每条发现都带补救建议(verify_buffer_init.cc:384-389 对 gemm 累加器 专门给 T.clear(C) / clear_accum=True),而不是所有发现共用一句话。

code/03-verify-pass-template/verify_local_alloc_limit.cc 是照着这三块写出的第三个 verify pass,附四条边界用例。验证级别:仅静态核对,未编译。


8. 代码风格与评审要求

8.1 C++:docs/developer_guide/cpp_style.md

CONTRIBUTING.md:40-62 把风格要求指向 docs/developer_guide/cpp_style.md。 写 pass 时最常撞上的六条(都能在那个文件里找到对应小节):

规则 例子
文件名 lower_snake_case lower_tile_op.cc、layout_reducer.h
类型 PascalCase,函数 PascalCase LayoutNode、InferLayout、MakeLinearLayout
布尔函数 Is/Has/Can + PascalCase IsSharedBuffer、HasTmaBarrier
参数与局部变量 lower_snake_case layout_map、thread_bounds
私有/保护成员 lower_snake_case_ layout_remap_、analyzer_
常量与枚举值 kPascalCase kWarpSize、kAccessRead

三条容易违反、且会被评审直接打回的:

  • handle 类型跨函数边界用 Stmt / Buffer / Var,*Node 指针只在 visitor 回调和局部检查里出现。 别把 const ForNode * 存进成员变量——节点会被 CopyOnWrite() 替换,存下来的指针会失效。
  • 比较身份用 .same_as(other),不要用 .get() ==。
  • 不要用 T 当参数名。 在 C++ 侧 T 通常指 tile op 的模板参数,在 Python 侧 T 是 DSL 命名空间;同名会让读者错乱。

另外注意一条与 FFI 有关的兼容性提醒:被反射的字段名是 Python/调试接口的一部分 (cpp_style 的「ObjectNode Fields」一节)。改 def_ro("src", ...) 里的名字属于 破坏性改动,即使编译能过。

8.2 Python 与格式化

  • 格式化:bash format.sh --files <changed-file>...(CONTRIBUTING.md:42-46)。 Python 走 Ruff/pre-commit,C/C++ 走 clang-format。风格改动要和行为改动分开, 否则 diff 无法评审。
  • 提交前跑 pre-commit run --all-files(CONTRIBUTING.md:111-117)。
  • 有条件时跑一次建议性的 C++ API 审计: python3 maint/scripts/audit_cpp_api_style.py(CONTRIBUTING.md:53-62)。 它不是批量改名工具,而是评审辅助——每条命中都要先判断 FFI 可见性、生成代码、 后端 shim 与外部 API 约束,再决定改不改。

8.3 maint/ 里与 pass 相关的自检

maint/ 不是「杂物间」,它装着几套金标准(golden)测试,改 pass 时非常有用:

目录 作用
maint/layout_inference/ 构造好的 IR 用例 + 已评审的期望 layout。README 说得很清楚:任何改 cost model 的改动都可能静默翻转胜出的 layout,金标准让这种翻转变成一次可评审的 diff,而不是运行时性能谜案
maint/gemm/ GEMM 正确性评估脚本(含 sm70 / tcgen05 / tcgen05-2cta / fp4 unpacked 变体)
maint/precision/ 精度对照(TileLang / Triton / PyTorch / CUDA,Precise vs Fast)
maint/host_checks/ 10 个故意触发 host 侧校验错误的最小复现脚本,配套 docs/compiler_internals/tensor_checks.md
maint/scripts/ CI、性能回归、风格审计、wheel 冒烟等入口脚本

8.4 一个 pass PR 应该带什么

评审人会按这个顺序看:

  1. 文件头注释:报什么、不报什么、精度边界在哪(对照 verify_buffer_init.cc:1-16 的写法)。
  2. 它在 pipeline 里的位置,以及为什么必须在那里(对照 2.5 节的表)。
  3. 挂了几个后端。只挂 CUDA 而在昇腾上静默失效,是高频问题。
  4. 测试:IR 基线有没有、边界用例有没有、端到端那条有没有标 requires_cuda / 昇腾前提。
  5. 开关:误报可能性 + 是否提供了 pass config(对照 3.4 节)。
  6. 风格:命名、handle/Node 用法、.same_as、反射字段是否被动过。
  7. 性能影响:编译器自身耗时与产物性能,各给一句话结论与测量方式。

9. 昇腾侧的额外要求

9.1 两棵树的关系

本教材里同时存在两套源码:

树 路径 内容
TileLang 主干(含昇腾后端) _vendor/tilelang-v0.1.15/ src/ascend/transform/ 31 个 .cc + auto_schedule/;Python 侧 tilelang/ascend/(pipeline.py、language/、analysis/、transform/)
TileLang-Ascend 适配器仓库 _vendor/tilelang-ascend-v0.1.2.000/ 一棵自带 src/transform/ 与 tilelang/transform/ 的分支树(ascend_sync_insert.cc、ascend_storage_rewrite.cc、ascend_memory_planning.cc、cross_core_pipeline.cc …),接口仍是旧的 tvm._ffi._init_api

版本按 版本锁定:适配器仓库的 tag 是 TileLang-ascend-v0.1.2.000-release,但仓库内 VERSION 文件写的是 0.1.4—— 以 tag 为准。第 20 章已经用差集说明了「两棵树的 pass 词汇不同」。

9.2 改哪一棵?

判断顺序:

  1. 你要改的是通用 pass(src/transform/ 下的)?→ 改主干,并且在适配器仓库 里同步对应文件,否则两棵树继续漂移。
  2. 你要改的是昇腾专属 pass?先看主干 src/ascend/transform/ 里有没有同名 pass。 有 → 改主干;这是 v0.1.15 之后的主线(tilelang/ascend/transform/__init__.py 的 __all__ 已经列出 30 个包装函数)。没有 → 说明这个 pass 还在适配器路线里。
  3. 你要改的是调度/同步算法?主干侧在 src/ascend/transform/auto_schedule/ 与 insert_sync.cc;适配器侧在 cross_core_pipeline.cc、ascend_sync_insert.cc。 两条路线的方法论见第 23、25 章。

一个可现场执行的对照(不用编译):同一个功能名,在两棵树里 grep 一下,看它落在 哪一侧、用哪个 FFI 接口。

# 主干
grep -rn "InsertNd2Nz" _vendor/tilelang-v0.1.15/src/ascend/transform/ \
                        _vendor/tilelang-v0.1.15/tilelang/ascend/transform/
# 适配器
grep -rn "SyncInsert\|sync_insert" _vendor/tilelang-ascend-v0.1.2.000/src/transform/

9.3 适配器仓库 AGENTS.md 的硬约束

_vendor/tilelang-ascend-v0.1.2.000/AGENTS.md:59-75 的四条通用原则,字面上就是给 AI 协作者写的,但同样约束人:

  1. 如实报告,禁止伪完成——未验证的结果不得表述为「已完成」或「已通过」。 这与本章 1.3 节的第二条纪律是同一条。
  2. 先验证,再下结论——能通过代码/文件/日志/测试确认的优先基于证据判断。
  3. 区分事实、推断与建议——禁止编造文件、输出、报错、性能收益。
  4. 遵循最小必要改动——优先复用既有实现与模式,不无依据地重写或扩面。

以及 :79-113 的核心算子开发原则里与「改编译器」直接相关的三条:不要凭记忆猜 API(先查 API 速查、再查 examples/、最后查源码,且 tilelang/language/pto.py 已废弃禁止使用);遇到错误先分析原因、不绕过门禁;代码仓探索必须使用 subagent(这条约束的是多代理工作流,不是人的工作方式)。

命名规范见 AGENTS.md:117-122:Python 用 PEP 8 + 类型注解、行宽 100;C++ 用 Google Style + clang-format、行宽 100;文件名 snake_case.py / snake_case.cc / test_<模块>_<功能>.py。这与主干的 cpp_style.md 一致, 但行宽 100 与测试命名是适配器仓库额外明确的。

还有一处对写 pass 的人最直接:AGENTS.md:24-28 的 Skills 索引里,Pass 分析与 设计被单独列成一类,包含 tilelang-pass-analyzer(功能分析 / 分类查询)、 tilelang-pass-workflow-analyzer(工作流、执行顺序、依赖关系、新 Pass 定位)、 tilelang-pass-design(设计方案与实现模式)。对应实现在 .agents/skills/ 下(另有 tilelang-pass-generate)。 也就是说,适配器仓库把「我要加一个 pass,它该放哪一段」这件事显式写进了它的 工作约定——这与本章 2.5 节的顺序判断表是同一个问题,只是它给了工具化入口。

9.4 昇腾侧的测试与基准入口

# 单测(不加载 CUDA 插件、不探测 GPU)
bash maint/scripts/run_local_ci_test_ascend.sh

# 性能回归(基线默认取 tile-ai/tilelang 的 main)
./maint/scripts/run_perf_regression_ascend.sh

昇腾侧单测的目录是 testing/ascend/,里面按主题分区: transform/(15 个 pass 测试)、language/、layout/、analysis/、 auto_schedule/、runtime/、cache/、target/。新加昇腾 pass 的测试放 testing/ascend/transform/test_ascend_<名字>.py,用 testing/ascend/_ir.py 的助手构造 IR。

适配器仓库的入口与主干不同,别混用:它没有 maint/scripts/run_* 这一套, 而是 maint/scripts/ci_performance.py / performance.py(性能)与 ci/operator_test_manifest.yaml(算子测试清单);testing/ 下只有 cpp/ 与 python/。改适配器之前先把这两套入口对上号。


10. 改动类型 → 需要跑的测试 / 基准 → 评审关注点

这张表是本章的落脚点。它把前面九节压成一张可以贴在显示器旁边的对照表。

改动类型 必须跑的测试 必须跑的基准 评审关注点
新增 Python pass(改 IR) IR 基线(assert_structural_equal)+ 单元测试 + 至少一条端到端对拍 至少一个受影响算子的 benchmark;若影响所有 kernel,跑 run_perf_regression.sh 挂在哪几个后端?顺序对不对(2.5 表)?name 传了吗?span 保住了吗?
新增 C++ pass(改 IR) 同上,另加:重载名拼写(必须有一条会命中的用例)、CreatePrimFuncPass 的 opt_level 同上,另加 TILELANG_PASS_PROFILE=1 看编译器自身耗时 注册名三处一致;src/op/builtin.h 是否需要新 config;handle/Node 用法;是否该用 ICHECK
新增 verify / check pass IR 层验证:必须命中 + 至少 5 条边界反向用例(5.5 表)+ 一条「不打断真实编译」的端到端用例 通常不需要(它不改 IR);但要在 CI 里跑一次全量编译确认不引入噪声 默认开还是关?误报怎么办(开关)?报告是否聚合?有没有给补救建议?折不动时是否保持沉默?
修改现有 pass 的算法 该 pass 已有的全部测试 + 新增一条覆盖本次行为变化的用例 该 pass 影响的所有算子;用 lower_trace 确认下游 diff 可控 改的是「判定」还是「重写」?旧行为有没有被依赖(金标准)?maint/layout_inference/ 这类 golden 是否翻转?
新增 pass config key 开关打开/关闭两条用例(对照 test_..._verify_buffer_init.py:458-483) 不需要 四处是否都改了(3.4 节)?默认值是否写进 docstring?PassConfigKey 枚举名与 C++ 常量字符串是否一致?
调整 pass 在 pipeline 中的顺序 全量 testing/python/transform/ + 端到端对拍 必跑 run_perf_regression.sh(顺序变化最容易既改性能又改正确性) 有没有违反 2.5 表里的前置依赖?是否为某个后端单独调序(会导致后端行为分叉)?
新增昇腾专属 pass testing/ascend/transform/test_ascend_<名字>.py;可用 testing/ascend/_ir.py 手搓 IR,不依赖前端 昇腾 benchmark;必要时 run_perf_regression_ascend.sh 放在 src/ascend/transform/ 而不是通用目录;USE_ASCEND=ON 才能编译;自动调度段之前还是之后(第 23 章)?
改适配器仓库的 pass 适配器自己的 testing/ + 主干侧确认没有同名漂移 适配器的 benchmark 脚本 用的是旧 FFI(tvm._ffi._init_api);是否需要同步回主干;两棵树的版本号会继续分叉
只改注释 / 文档 不需要 不需要 注释是否与代码一致;有没有引入「最新版」这类模糊说法
只加测试 新测试必须先在没有改动的树上跑一遍并确认它是绿的 不需要 测试是否真的在测行为,而不是在测实现细节(例如断言了会变的日志前缀)

一个必须重复的判断:这张表的每一行都要求「先在没有改动的树上跑一遍」。 一个从来没绿过的测试,比没有测试更危险。


11. 本章小结

  1. 一个 pass = 定义 + 实现 + 注册 + 挂载 + 测试,五件事缺一不可。缺注册 → AttributeError;缺挂载 → 静默不生效;缺测试 → 上面两条都发现不了。
  2. 双语结构有明确的判断规则:需要 C++ 独有语义(tile op、layout 对象、 BufferRegion)就写 C++;改动频繁、需要快速迭代就写 Python。v0.1.15 里 src/transform/ 有 42 个顶层 .cc、src/ascend/transform/ 有 31 个, Python 侧真正的 pass 是 tilelang/transform/ 下 3 个加 tilelang/analysis/、tilelang/ascend/analysis/、tilelang/metal/transform/ 下的一批检查器。
  3. FFI 的通路是「注册名 = 前缀 + 属性名」:C++ 的 refl::GlobalDef().def("tl.transform.X", X) ↔ Python 的 tvm_ffi.init_ffi_api("tl.transform", __name__) ↔ _ffi_api.X ↔ 薄包装 tilelang.transform.X。改名要同时改三处,其中一处改错不报错。
  4. 挂载是一行函数调用,顺序硬编码在每个后端的 pipeline.py 里;通用 pass 要在 6 条 pipeline 里各加一行。
  5. 测试有四层:IR diff 基线(最严格)、单元测试(行为)、边界条件(防误报与 漏报)、端到端对拍(防打断真实编译)。每一层能证明的东西不同,缺一层就漏一类错。
  6. 性能回归有明确的判定规则:不是看单条数字,而是 「≥ MIN_COUNT(2) 条 speedup < THRESHOLD(0.90)」。定位顺序是 benchmark → 重跑排除噪声 → pass_timing 分编译期/产物 → lower_trace 找 diff。
  7. 昇腾要额外处理两棵树:主干 src/ascend/transform/ 是主线,适配器仓库是 另一套词汇 + 旧 FFI;AGENTS.md 的四条通用原则(如实报告、先验证、区分事实与 推断、最小必要改动)与本章的纪律是同一条。

延伸阅读

  • 本章代码与图:资料库索引、figures/fig-01-* ~ fig-04-*。
  • 上游一手材料与逐条核验状态:见 本章引用——源码引用 S1–S50、昇腾双树 A1–A5、 corpus 资料 R1–R6、本章自产材料 C1–C5、上游链接 U1–U9,以及第十二节的「待补资料」。
  • 想继续往下读:第 30 章讲只有 main 上才有的方向(多后端解耦、CuteDSL 后端、 T.PersistentKernel 等),第 31 章把「从 issue 到 PR」的完整流程与三种贡献 (改编译器 / 加后端 / 加算子)的检查清单串起来。
  • 相邻章节回顾:第 20 章(读 pass 与 IR)、第 12 章(调试与性能分析)、 第 21–23 章(layout / 流水同步 / 昇腾自动调度)、第 25 章(昇腾后端)。

习题

11 题 · 答案在提交后展开

先在纸上或输入框里写下你的答案,再展开参考答案对照。客观判断请自己打勾:这三颗按钮 只影响本机保存的学习进度。

E1概念难度 ★无需硬件概念题

下面五个 pass,各自属于 C++ 侧还是 Python 侧?给出你的判断依据(不要只给答案), 并说明如果把它们互相搬家会失去什么:

  1. HoistBroadcastValues
  2. LayoutInference
  3. AddWrapperForSingleBufStore
  4. VFChecker
  5. z3_schedule_python(注意:它出现在 tilelang/ascend/transform/z3_scheduler.py 里)

需要硬件:否


E2概念难度 ★★无需硬件概念题

某人把一个 C++ pass 的注册名从 "tl.transform.Foo" 改成了 "tl.transform.Bar",同时把 Python 薄包装写成 def Bar(): return _ffi_api.Bar(),但忘了改 CreatePrimFuncPass(pass_func, 0, "tl.Foo", {})。

请回答:

  1. 编译会报错吗?运行时的哪一步会报错?
  2. 有哪些地方表现正常?为什么这件事比「直接报 AttributeError」更麻烦?
  3. 在本章 2.3 节列的「三处一致」里,这一处对应的是哪一处,为什么它偏偏最不容易 被发现?
  4. 昇腾适配器仓库(_vendor/tilelang-ascend-v0.1.2.000/)的 tilelang/transform/_ffi_api.py 与 v0.1.15 的写法有什么不同?如果直接把适配器 里的一段 pass 代码拷进 v0.1.15,C++ 注册部分需要改什么?

需要硬件:否


E3代码阅读难度 ★★无需硬件代码阅读题

读 _vendor/tilelang-v0.1.15/src/transform/verify_buffer_init.cc:315-321:

void VisitStmt_(const BufferStoreNode *op) final {
  const Object *saved = active_store_;
  active_store_ = op;
  StmtExprVisitor::VisitStmt_(op);
  active_store_ = saved;
  written_.insert(op->buffer->data);
}

请回答:

  1. 为什么 written_.insert(...) 在 VisitStmt_ 之后而不是之前?这个顺序 保证了哪一条语义?
  2. active_store_ 的作用是什么?结合 VisitExpr_(const BufferLoadNode *op) (:323-328)说明它解决了哪个具体问题。
  3. 如果把这个函数的顺序反过来(先 insert 再 visit), test_reports_a_store_that_reads_its_own_destination (testing/python/transform/test_tilelang_transform_verify_buffer_init.py:183-201) 会变成通过还是失败?
  4. 文件头注释(:11-15)说这个分析「deliberately imprecise in one direction」, 指的是哪个方向?为什么这个取舍能让检查默认打开?

需要硬件:否


E4代码阅读难度 ★★无需硬件代码阅读题

读 _vendor/tilelang-v0.1.15/tilelang/transform/add_bufstore_wrapper.py:104-159 (pre_visit / post_visit / ir_transform 三段)。

请回答:

  1. 为什么 tile_operation_depth 必须在 post_visit 里 -= 1,不能只在 pre_visit 里 += 1?
  2. thread_binding_vars 是干什么用的?为什么命中它就不做改写?
  3. 这个 pass 的最后一行是 return prim_func_pass(pass_fn, opt_level=0, name="tl.AddWrapperForSingleBufStore"), 而 hoist_broadcast_values.py:99 只写了 return prim_func_pass(pass_fn, opt_level=0)。这个差别会在哪个工具的输出里 被看到?请说出工具名与它会显示什么。
  4. 为什么这个 pass 必须挂在几乎所有 pipeline 的最前面 (cuda/pipeline.py:79、ascend/pipeline.py:58)?

需要硬件:否


E5动手难度 ★★无需硬件动手题

仿照 code/01-python-pass/hoist_constant_broadcast.py,写一个 Python pass CountSerialLoops(),统计每个 PrimFunc 里 ForKind.SERIAL 的循环数量,并在 name="tl.CountSerialLoops" 下注册(Python 侧直接 export 到 tilelang.transform,不需要 FFI)。

要求:

  1. 先用 python3 -m py_compile 过一遍语法;
  2. 写至少两条测试:一条命中(有 serial 循环)、一条反向(只有 parallel 循环时 不报);
  3. 在注释里用一行说明:为什么这个 pass 写在 Python 侧是合理的。

需要硬件:否(静态检查)/ 是(若要用 pytest 真正运行)


E6动手难度 ★★★无需硬件动手题

code/03-verify-pass-template/verify_local_alloc_limit.cc 检查的是 「on-chip 分配元素数超限」。请把它改造成检查另一件事: 「同一块 local.fragment 被写入之后、在被任何 pass 读之前,必须至少被写过两次」 (即:单写未读的 fragment 是可疑的)。

要求:

  1. 先不要写代码,先写出判定规则的一句话描述与精度边界(什么情况你会故意 不报);
  2. 列出至少 5 条边界反向用例,说明每一条为什么必须存在(可参考本章 5.5 节的表);
  3. 再写出 C++ 骨架的关键部分(visitor 重载签名 + Report 结构 + EmitReport 的措辞)。不需要完整可编译。

需要硬件:否


E7概念难度 ★★无需硬件概念题

你写了一个新 pass,它要读 T.copy 写入 shared buffer 的 src/dst region, 用来推断是否存在冗余拷贝。

请回答:

  1. 它必须放在哪个 pass 之前?为什么?
  2. 如果它同时需要 layout 信息(例如需要知道 shared buffer 的 swizzle), 这个顺序约束会不会冲突?冲突时你会怎么办?
  3. 该 pass 需要被挂到 CUDA 与 Ascend 两条 pipeline 上,还是只挂一条?说出判断 依据,并列出 v0.1.15 里所有后端 pipeline 文件。
  4. 用一句话说明:为什么 TileLang 用「命令式 pipeline + 硬编码顺序」,而不是 像 LLVM 那样做 pass 依赖解析?请从可读性与可调试性两个角度各给一条 理由(提示:想想 TL_LOWER_TRACE 的报告是按什么顺序排的)。

需要硬件:否


E8概念难度 ★★无需硬件概念题

一次性能对比的结果是:40 个 case 里,case_A 的 speedup 是 0.85x,其余 39 个都是 1.00x。

请回答:

  1. 按 v0.1.15 的判定规则(maint/scripts/test_perf_regression.py:24-32,243-257), 这算不算回归?给出判定过程。
  2. 把 case_A 之外再加 case_B(0.88x),结论变了吗?
  3. 上游那段注释用什么数据支持了这个门槛?为什么它特意提到「最低 0.044x」这个 极端值?
  4. 如果这次对比里有一个 case 只在当前版本出现、基线里没有,脚本会把它算进 「回归」吗?对应的开关叫什么?
  5. 运行 python3 code/04-regression-judge/judge_regression.py --self-test, 把你看到的输出与你的第 1、2 问答案对照。

需要硬件:否


E9动手难度 ★★★无需硬件动手题

场景:你把某个 pass 加进了 pipeline,编译出来的 kernel 数值正确,但 E2E 延迟从 1.00 ms 掉到 1.14 ms;同一批 40 个 case 里有 6 个都掉了 10% 以上。

请:

  1. 写出判定这一步要跑的命令(含环境变量),并说明为什么不能只看你手上那一个 数字;
  2. 写出**区分「编译变慢」与「产物变慢」**的命令;
  3. 假设 TILELANG_PASS_PROFILE=1 显示某个外层 pass 的 inclusive 时间涨了 3 倍, 但 self 时间没变,你的下一步是什么?
  4. 假设 TL_LOWER_TRACE=terminal 显示你的 pass 之后 VectorizeLoop 的 diff 也变大了,这说明了什么?你会怎么继续缩小范围?
  5. 假如最终发现是「你的 pass 把循环切小了,导致向量化掉到 4 字节」,请说明你会用 四层测试里的哪一层来把这个回归固定住,并给出这条测试的形态(一句话即可)。

需要硬件:是(要真正跑 benchmark)


E10综合难度 ★★无需硬件评审题

你收到一个 PR:它新增了一个 C++ pass src/transform/verify_no_warp_divergence.cc, 只挂进了 tilelang/cuda/pipeline.py,带了一个 IR 基线测试和一个 requires_cuda 的端到端用例,并且在 src/op/builtin.h 里加了一个新 config key。

请列出至少 6 条你会提出的评审问题,并说明每条属于本章 8.4 节的哪一类关注点。 至少覆盖:后端覆盖、位置与顺序、开关的四处一致性、测试的边界、命名与风格、 以及「这个检查应该默认开还是默认关」。

需要硬件:否


E11综合难度 ★★无需硬件昇腾题

你要为昇腾加一条新的检查:「T.copy 的 GM→L1 搬运必须显式处理 OOB 尾部」。

请回答:

  1. 这条检查应该放在主干的 src/ascend/transform/ 还是适配器仓库的 src/transform/?给出判断依据(提示:先看主干里有没有做类似事情的 pass)。
  2. 你看中了 AscendInsertOOBPadding 的注释( tilelang/ascend/transform/__init__.py:154-163)里关于「必须跑在 InsertNd2Nz 之后、AutoSchedule 之前」的说明。请解释这两条顺序约束各自的 原因。
  3. 昇腾侧的单测入口脚本是哪一个?它跑哪两个目录?它与 CUDA 侧入口的两点行为差异 是什么?
  4. _vendor/tilelang-ascend-v0.1.2.000/AGENTS.md:59-75 的四条通用原则中,哪一条 与本章「所有结论必须标验证级别」是同一件事?再指出 :79-113 里哪一条会直接 影响你怎么查 API。

需要硬件:否

章节自测

7 题 · 提交后本地评分

单选题。提交后立即给出分数与逐题解析;重做会清空选择,但会保留上一次的成绩记录。

  1. 1. 你新写了一个 C++ pass,想让 Python 侧能用 tilelang.transform.YourPass() 调到它,必须做哪件事?

  2. 2. 把 VisitStmt_(const BufferStoreNode *op) 写成 VisitStmt(const BufferStoreNode *op),会发生什么?

  3. 3. 关于 IR diff 基线(tvm.ir.assert_structural_equal),下面哪句正确?

  4. 4. 一个新加的通用 pass(如 VerifyBufferInit)要在几条 pipeline 里各挂一行?

  5. 5. 仓库里判定「一次改动是否造成性能回归」的规则是什么?

  6. 6. 按模板加了一个新的 C++ pass 文件后,冒烟脚本仍报 AttributeError,最可能的原因是什么?

  7. 7. 给一个可能误报的检查加 pass config key(例如 tl.disable_buffer_init_check),一共要改几处?

本章代码

14 个文件

正文里提到的每个示例都能在这里直接阅读(带语法高亮),也可以整章浏览。

浏览本章全部代码 →

本章引用

源码链接指向锁定 tag

本章写到的每个源码位置与外部资料都在下面列出:源码链接指向锁定 tag 的 GitHub 文件, 资料链接指向原文。查看全部资料索引 →

展开引用清单

第 29 章 参考资料

本章所有技术结论都基于下表中的本地资料或锁定版本源码。版本以 版本锁定 为准:TileLang v0.1.15 (commit a35f8ddf45eba16c21211ec8822d56ce5363036f,VERSION = 0.1.15)与 TileLang-Ascend TileLang-ascend-v0.1.2.000-release (commit 89a9364e47a341fffdd65ed94786b1d4f9b6a78c)。

写作环境:macOS (Apple Silicon)、Python 3.11;没有 NVIDIA GPU、没有昇腾 NPU、 没有安装 tilelang/tvm(import tilelang → ModuleNotFoundError, command -v nvcc 无输出)。因此本章代码示例的最高验证级别是「仅静态核对」; 可离线运行的三个脚本是「CPU 逻辑验证」(见 code/ 下各 README)。 本章不包含任何自测性能数字,也不包含任何编译/运行结果。

核验日期均为 2026-10-01。核验方式:在锁定版本的工作树上用 grep -n / sed -n 定位定义行与行号,阅读完整签名、文件头注释与断言;计数类结论用脚本统计 (例如 ls src/transform/*.cc | wc -l、python3 -c 统计 __all__)。


一、双语结构与 FFI

id 引用位置 用途 核验状态
S1 VERSION 版本事实 = 0.1.15 已核验(2026-10-01)
S2 tilelang/transform/_ffi_api.py:1-6 全文件 6 行;:3 import tvm_ffi、:6 init_ffi_api("tl.transform", __name__) 已核验(2026-10-01):逐行读
S3 tilelang/transform/__init__.py:9-11(Python pass 的 import)、:19-26(PipelinePlanning)、:86-94(VerifyParallelLoop)、:97-108(VerifyBufferInit、ThreadSync) 薄包装与门面 已核验(2026-10-01)
S4 src/transform/verify_buffer_init.cc:400-422 Pass VerifyBufferInit()(:400-417)与 TVM_FFI_STATIC_INIT_BLOCK(:419-422) 已核验(2026-10-01):逐行读
S5 src/transform/thread_storage_sync.cc:2091-2110 带参数的 pass:ThreadSync(const String &storage_scope) 与注册 已核验(2026-10-01)
S6 src/ascend/transform/insert_nd2nz.cc:485-491 昇腾 pass 的 CreatePrimFuncPass(:485)与注册(:488-490) 已核验(2026-10-01)
S7 tilelang/ascend/transform/__init__.py:1-11(InsertNd2Nz)、:154-163(AscendInsertOOBPadding 的顺序约束)、:225-256(__all__,共 30 个名字) 昇腾 pass 的 Python 门面与 pass 数量 已核验(2026-10-01):__all__ 名字数由脚本统计
S8 _vendor/tilelang-ascend-v0.1.2.000/tilelang/transform/_ffi_api.py:1-8 适配器用旧接口 tvm._ffi._init_api(:8) 已核验(2026-10-01):逐行读
S9 src/transform/verify_buffer_init.cc:1-16 文件头注释:报什么、不报什么、精度边界 已核验(2026-10-01)
S10 tilelang/ir.py:105、:123 get_stmt_span / stamp_stmt_spans 已核验(2026-10-01)
S11 tilelang/ascend/transform/z3_scheduler.py:9、:228-229、:784-785 import tvm_ffi 与两个 register_global_func 的名字 已核验(2026-10-01)

二、Python pass 与 mutator(本章示例 A 的原型)

id 引用位置 用途 核验状态
S12 tilelang/transform/hoist_broadcast_values.py:16-72(mutator)、:75-99(pass 工厂,:99 无 name=) 示例 A 的简化原型 已核验(2026-10-01):逐行读
S13 tilelang/transform/add_bufstore_wrapper.py:1-159(:104-121 pre/post visit、:155-159 收尾与 prim_func_pass) 双访客 + 嵌套深度计数器的范例 已核验(2026-10-01)
S14 tilelang/transform/decouple_type_cast.py:1-44(文件头 before/after)、:107-135(_CastFinder)、:349-410(visit_for_)、:642-663(pass 工厂与顺序约束注释 :650-652) 完整 Python mutator 的范例 已核验(2026-10-01)
S15 tilelang/ascend/analysis/vf_checker.py:5、:173、:186 VFChecker 是 Python 写的 prim_func_pass 已核验(2026-10-01)

三、pipeline 与挂载

id 引用位置 用途 核验状态
S16 tilelang/cuda/pipeline.py:68、:79、:83-84、:93-98、:129-130、:134、:142、:150、:168、:284 顺序、守卫、PassPipeline 构造 已核验(2026-10-01):grep -n 逐点核对
S17 tilelang/ascend/pipeline.py:22-33、:36、:58、:61、:76、:95、:103-116、:125、:190 昇腾顺序、VFChecker 调用点、自动调度段 已核验(2026-10-01)
S18 tilelang/cpu/pipeline.py:35,50、tilelang/metal/pipeline.py:40,59、tilelang/rocm/pipeline.py:39,52、tilelang/webgpu/pipeline.py:41,54 VerifyBufferInit / DecoupleTypeCast 出现在 6 条 pipeline 中 已核验(2026-10-01):grep -rn
S19 tilelang/backend/pass_pipeline/pipeline.py:11-40 PassPipeline 与 pass_pipeline(name) 作用域 已核验(2026-10-01)
S20 tilelang/backend/pass_pipeline/pipeline_utils.py:25-76、:105 allow_* / should_* 守卫与 LayoutVisual 已核验(2026-10-01)
S21 tilelang/engine/lower.py:92-96、:117、:136、:162、:234 HoistBroadcastValues 的挂载点;三处 compile_pass_instrumentation 已核验(2026-10-01)

四、instrumentation、调试工具与 pass config

id 引用位置 用途 核验状态
S22 tilelang/instrumentation/session.py:130、:163、:213 工具注册、编译作用域、把工具装进 PassContext 已核验(2026-10-01)
S23 tilelang/instrumentation/events.py:1-50 pass_phase / PassEvent 的定义 已核验(2026-10-01)
S24 tilelang/tools/pass_timing.py:25(logger)、:64-73(PassTimingRecord 的 duration_s 与 self_duration_s)、:334(create_pass_timing_tool) 编译期耗时工具 已核验(2026-10-01)
S25 tilelang/jit/kernel.py:243 JIT 编译时装配 pass timing 工具 已核验(2026-10-01)
S26 tilelang/tools/lower_trace/__init__.py:49(lower_trace)、tilelang/tools/lower_trace/core.py:648、:663(create_pass_instrument / enable) IR Lower Trace 的程序 API 已核验(2026-10-01)
S27 tilelang/utils/pass_diff.py:1030(pass_diff) 旧 IR diff 工具的程序 API 已核验(2026-10-01)
S28 docs/tools/lower_trace.md、docs/tools/pass_diff.md(顶部「Superseded — use IR Lower Trace」admonition) 工具的官方说明与被取代关系 已核验(2026-10-01):逐行读顶部段落
S29 src/op/builtin.h:115-130(kDisableDataRaceCheck :115、kDisableBufferInitCheck :122、kEnableDumpIR :129、kDumpIRDir :130)、src/op/builtin.cc:28-42 pass config 常量的定义与注册 已核验(2026-10-01)
S30 tilelang/transform/pass_config.py:10、:52-68、:344-347 PassConfigKey 枚举、两个检查开关、TL_PASS_PROFILE* 已核验(2026-10-01)

五、测试

id 引用位置 用途 核验状态
S31 testing/python/transform/test_tilelang_transform_verify_buffer_init.py:54-58(_verify)、:106-141(extern / global / 参数 buffer)、:183-201(自读自写)、:218-237(跨线程作用域)、:358-371(只报一次)、:377-388(no_kernel_cache)、:447-483(requires_cuda 端到端) verify pass 的测试范式与边界清单 已核验(2026-10-01):逐段读
S32 testing/python/transform/test_tilelang_transform_verify_parallel_loop.py:7-70 手搓 IRModule + capfd 断言 + span 断言 已核验(2026-10-01)
S33 testing/python/transform/test_tilelang_transform_hoist_broadcast_values.py:56-64(_check)、:67-133(四个 before/after) IR diff 基线的范式 已核验(2026-10-01)
S34 testing/ascend/_ir.py:1-58 昇腾侧 IR 构造助手(seq / region / copy / kernel / nodes / calls) 已核验(2026-10-01)
S35 testing/ascend/transform/test_ascend_insert_nd2nz.py:11-68 昇腾 pass 测试的范式(含 pytest.raises 的存储约束用例) 已核验(2026-10-01)
S36 testing/ascend/ 目录清单(transform/ 15 个测试文件 + language / layout / analysis / auto_schedule / runtime / cache / target) 昇腾测试分区与数量 已核验(2026-10-01):ls

六、性能回归

id 引用位置 用途 核验状态
S37 maint/scripts/run_perf_regression.sh:1-32(用法与环境变量)、:150-168(baseline worktree)、:240-369(wheel 缓存)、:412-493(跑两遍 + 对比 + 汇总) 「改动前后回归判定」的主入口 已核验(2026-10-01):逐段落读
S38 maint/scripts/test_perf_regression.py:24-32(THRESHOLD / MIN_COUNT 与标定注释)、:243-257(判定与措辞) 判定规则的唯一出处 已核验(2026-10-01)
S39 maint/scripts/compare_perf_regression.py:20-90(payload 解析与 speedup)、:293、:303、:324(--fail-on-regression 与阈值) 对比器与退出码 已核验(2026-10-01)
S40 maint/scripts/run_current_regression.py:44-56(git 状态字段)、:104-146(缓存 payload 结构:results / failures / git) --old / --new 输入 JSON 的 schema 已核验(2026-10-01):grep -n 核对字段名
S41 tilelang/testing/perf_regression.py:16-23(PerfResult)、:89-106(process_func)、:109-159(regression() 扫描 regression_* 入口) 用例侧的写法规约 已核验(2026-10-01)
S42 maint/scripts/run_perf_regression_ascend.sh:1-20、maint/scripts/run_local_ci_test_ascend.sh:1-20 昇腾侧基准与单测入口(与 CUDA 侧的两点差异) 已核验(2026-10-01)
S43 _vendor/tilelang-v0.1.15/benchmark/(matmul/、matmul_fp8/、mamba2/、blocksparse_attention/、compile_speed/、matmul_metal/) 单算子 benchmark 目录清单 已核验(2026-10-01):ls

七、代码风格、构建与评审

id 引用位置 用途 核验状态
S44 docs/developer_guide/cpp_style.md(corpus 副本:TileLang 官方文档 · 资料清单) 命名、TVM object/handle 用法、*Node 指针边界、反射字段兼容性 已核验(2026-10-01):读「Formatting / Naming / TVM Object And FFI Types / ObjectNode Fields」四节
S45 CONTRIBUTING.md:40-62(风格与审计)、:64-93(开发环境)、:111-127(lint 与测试命令) 贡献流程的官方说明 已核验(2026-10-01):逐行读
S46 CMakeLists.txt:374-389(file(GLOB TILE_LANG_SRCS ...))、:469(add_library(tilelang_objs ...)) 「新增 .cc 要重新 configure」的依据 已核验(2026-10-01)
S47 src/ascend/CMakeLists.txt:1-42(:15-20 always 源、:22 if(NOT USE_ASCEND) return()、:32-38 active 源含 src/ascend/transform/*.cc) 昇腾条件编译与「缺对象类型会 Cannot find object type index」 已核验(2026-10-01)
S48 资料库索引 golden layout harness:为什么 cost model 的改动需要金标准 已核验(2026-10-01):读全文
S49 资料库索引、资料库索引 host 侧 10 个故意报错的复现脚本;精度对照表 已核验(2026-10-01):读开头段落
S50 maint/scripts/audit_cpp_api_style.py、maint/scripts/apply_mit_license.sh、maint/scripts/check_mit_license.sh maint/scripts/ 里的自检脚本清单 已核验(2026-10-01):ls;audit_cpp_api_style.py:601 是 main()

八、昇腾双树与适配器仓库(_vendor/tilelang-ascend-v0.1.2.000/)

id 引用位置 用途 核验状态
A1 AGENTS.md:59-75(四条通用原则)、:79-113(核心算子开发原则,含「不要凭记忆猜 API」:83-87)、:117-122(命名与行宽) 适配器仓库的硬约束 已核验(2026-10-01):逐行读
A2 AGENTS.md:24-28(Pass 分析与设计 skills:tilelang-pass-analyzer / -pass-workflow-analyzer / -pass-design)、.agents/skills/(含 tilelang-pass-generate) 「新 Pass 定位」被显式写进工作约定 已核验(2026-10-01):ls .agents/skills
A3 VERSION(仓库内写 0.1.4)、tag TileLang-ascend-v0.1.2.000-release 版本以 tag 为准 已核验(2026-10-01):与 版本锁定 一致
A4 src/transform/ 清单(ascend_sync_insert.cc、ascend_storage_rewrite.cc、ascend_memory_planning.cc、ascend_lower_parallel_to_vector.cc、cross_core_pipeline.cc、ascend_combinecv.cc …) 适配器是另一套昇腾 pass 词汇 已核验(2026-10-01):ls
A5 maint/scripts/(ci_performance.py、performance.py、…)、ci/operator_test_manifest.yaml、testing/(仅 cpp/ 与 python/) 适配器自己的测试/性能入口,与主干不同 已核验(2026-10-01):ls

九、corpus 资料

id 引用位置 用途 核验状态
R1 tilelang-benchmark/README.md 公共算子集的形状表(GEMM / FlashAttention / Linear Attention)与标题里的 Archived 声明 已核验(2026-10-01):读全文
R2 tilelang-benchmark/debug.py 上游用来给结果加 ±5% 抖动的辅助脚本 已核验(2026-10-01):读全文;正文未引用其数值
R3 TileLang 官方文档 · 资料清单 官方 C++ 风格指南的 corpus 副本(与 _vendor/ 内文档一致) 已核验(2026-10-01):与 _vendor/tilelang-v0.1.15/docs/developer_guide/cpp_style.md 对照
R4 TileLang 官方文档 · 资料清单、.../tools/pass_diff.md 两个调试工具的官方文档副本 已核验(2026-10-01)
R5 tile-ai/tilelang/(testing/python/debug/test_pass_timing.py、test_pass_diff.py、test_pass_events.py、tilelang/tools/pass_timing.py …) corpus 里的源码镜像;版本可能与 v0.1.15 不同,正文一律以 _vendor/ 为准 已核验(2026-10-01):仅用于确认文件存在
R6 TileLang 昇腾后端 · 资料清单(tile-ai/tilelang-ascend 等) 适配器仓库的一手镜像 已核验(2026-10-01):仅作背景

十、本章自产材料

id 位置 内容 验证级别
C1 code/01-python-pass/ 最小 Python pass HoistConstantBroadcast + 三个 IR 对照用例 仅静态核对(py_compile 通过;API 名逐个在 v0.1.15 核对)
C2 code/02-cpp-pass-template/ C++ pass 模板 CountParallelLoops + 包装片段 + pytest 骨架 + 构建步骤 仅静态核对(未编译)
C3 code/03-verify-pass-template/ verify pass 模板 VerifyLocalAllocLimit + 边界用例 + 骨架抽取脚本 抽取脚本:CPU 逻辑验证(已运行);C++ 与 pytest:仅静态核对
C4 code/04-regression-judge/ 按 v0.1.15 规则判定回归的小工具(--self-test 已运行) CPU 逻辑验证(已运行)
C5 figures/fig-01 … fig-04 四张自绘图(双语结构与 FFI / pass 骨架解剖 / 四层测试 / 回归定位流程) 自绘;行号取自 _vendor/,2026-10-01 核对

十一、上游链接(本机以 curl -o /dev/null -w '%{http_code}' -L 核验)

id URL 用途 核验状态
U1 https://github.com/tile-ai/tilelang 主干仓库 已核验(2026-10-01,HTTP 200)
U2 https://github.com/tile-ai/tilelang-ascend 昇腾适配器仓库 已核验(2026-10-01,HTTP 200)
U3 https://github.com/tile-ai/TileOPs tilelang-benchmark README 指向的继任仓库 已核验(2026-10-01,HTTP 200)
U4 https://github.com/tile-ai/tilelang-benchmark 公共算子集 benchmark 仓库(已归档) 已核验(2026-10-01,HTTP 200)
U5 https://github.com/tile-ai/tilelang/blob/v0.1.15/docs/developer_guide/cpp_style.md C++ 风格指南(v0.1.15 tag) 已核验(2026-10-01,HTTP 200)
U6 https://github.com/tile-ai/tilelang/issues?q=is%3Aissue+is%3Aopen+label%3A"♥+help" CONTRIBUTING.md:33 提到的 ♥ help 标签 已核验(2026-10-01,HTTP 200)
U7 https://tilelang.com/get_started/Installation.html CONTRIBUTING.md:109 引用的安装文档 已核验(2026-10-01,HTTP 200)
U8 https://tilelang.com/tools/lower_trace.html IR Lower Trace 的在线文档 已核验(2026-10-01,HTTP 200)
U9 https://tilelang.com/tools/pass_diff.html Pass Diff 的在线文档 已核验(2026-10-01 终审复验 curl -L 返回 HTTP 200;写作时曾超时,结论仍以本地 docs/tools/pass_diff.md 为准)

十二、待补资料

缺什么 为什么需要 建议来源
官方「如何写一个 pass」的教程 v0.1.15 的 docs/ 里没有这样一篇:docs/developer_guide/ 只有 cpp_style.md,docs/compiler_internals/ 只有 4 篇(inject_fence_proxy / letstmt_inline / metal_tilelang_development / tensor_checks)。本章 3.2–3.4 的「五步法」是从源码归纳的,不是引用官方教程 上游若新增 docs/contributing/writing-a-pass.md 之类的文档;或维护者的一份 issue 说明
一次真实编译 + 测试的完整日志 本章所有「能编译 / 测试通过」的说法都停在待验证。需要一台有 CUDA 的机器,把 资料库索引 的六步跑通并留档 任一有 sm_80+ GPU 与 nvcc 的开发机
maint/scripts/run_perf_regression.sh 的实测输出样例 本章只能引用判定规则与脚本结构,给不出真实报告样例 上游 CI 产出的 PR 机器人报告(maint/scripts/run_perf_regression.sh 的运行结果)
适配器 .agents/skills/ 下 4 个 pass 相关 skill 的正文 AGENTS.md:24-28 只列了名字与一句话职责;要判断它们与本章方法论是否一致,需要读 SKILL.md 正文 适配器仓库 .agents/skills/tilelang-pass-*/

本章共 14 个二级小节;右上角目录在桌面端会跟随滚动高亮。