跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 23 章编译期、后端与芯片32 分钟12 习题7 自测题

自动调度与约束求解(昇腾)

昇腾后端最有技术含量的一步:把 kernel 拆成带资源标签的任务、估计延迟与容量、用 Z3 求解调度与多缓冲,再把解落成真实的 Set/Wait。讲清模型、约束与它失效时的后果。

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

学习目标

读完本章,你应当能够:

  1. 说清 Ascend C 里「人肉维护同步与多缓冲」的三类痛苦——Set/Wait 配对、Cube/Vector 生产者—消费者关系、跨迭代复用同一块 UB/L1——并解释为什么这三件事可以一起写成 一个调度问题。
  2. 把一段 kernel 拆成带资源标签的任务。标签只有七个:MTE1 / MTE2 / MTE3 / Cube / Vector / Fixpipe / Scalar;你要能写出「任务 + 依赖边 + 延迟估计 + 片上容量」这套 四元模型,并说出每一部分对应哪个源码文件。
  3. 数出 v0.1.15 里 AutoSchedule 前后一共 13 个 pass,说出每个 pass 的职责与文件 位置,并能用 code/pass_order/ 里的脚本自己核一遍。
  4. 写出 Z3 在这里解的是什么:整数变量是什么、布尔变量是什么、约束是什么、目标是什么; 并解释为什么 II(相邻两轮迭代的启动间隔)和 buffer 份数必须一起解。
  5. 把一个调度解一路追到 IR:求解结果 → buffer 份数 → set_flag/wait_flag → AIV/AIC 的最终 body。
  6. 说明为什么 CUDA 侧不需要这一步,并指出这里的关键差异是硬件同步语义与 编译器已有基础设施,不是「NV 更简单」。
  7. 列出模型不准确、约束写错、求解失败三种情况各自的后果,并给出在有 NPU 的机器上 验证本章结论的实验步骤。

前置知识

  • 第 6 章:Cube/AIC 与 Vector/AIV 的分工、GM→L1→L0A/L0B→Cube→L0C→Fixpipe 与 成对 Set/Wait 的硬件背景。本章不再重复这些名词的定义。
  • 第 11 章:T.Pipelined 的软件流水语义与 num_stages 的直觉。
  • 第 20 章:TIR 结构与 pass pipeline 的组织方式,以及怎么看一条 pass 链。
  • 第 22 章:流水线规划、多缓冲与同步插入的一般原理(NV 侧怎么做)。本章只讲昇腾 这一侧的求解部分。

你不需要学过 SMT/Z3。本章会把用到的每一类约束都写成一行可以手算的不等式。 你只需要会 Python、写过 PyTorch。

本章的验证边界。 写作环境是 macOS(Apple Silicon),没有 NVIDIA GPU、 没有昇腾 NPU,也没有安装 z3-solver(见 本章引用 V8)。因此:

  • 本章没有一条性能数字是本机测出来的;引用的常量都是源码里的默认参数或社区文章的 说法,逐条标注来源与条件。
  • 本章给出的「最小可复现玩具模型」用纯 Python 实现并在本机跑过,它复现的是 z3_scheduler.py 的约束语义,不等于验证了 Z3 或 TileLang 的行为。
  • 凡是无法在 _vendor/tilelang-v0.1.15/ 里找到依据的说法,一律标「待核」。

1. 问题的由来:Ascend C 里最难的不是写,是排

1.1 一张必须一直放在脑子里的时间表

在 Ascend C 里写一个带流水的算子,难点通常不在数学,而在同步。社区里流传很广的一段 描述来自知乎《DeepSeek 开源昇腾基础组件将会带来哪些影响?》(来源 id zhihu-dazhiruoyu-deepseek-ascend,community 级,非厂商文档):

「大家真正写过昇腾应该都有体会,Ascend C 难写的地方很多,但最折磨人的往往还是同步, 因为 MTE、Cube、Vector 都是相对独立的流水,一边要靠 Set/Wait 保证不同 Pipeline 之间 的数据真正 ready,一边还要处理 Cube 和 Vector 之间的生产消费关系,再叠上 double buffer、跨迭代流水以后,程序员脑子里实际上一直维护着一张时间表:这一轮 MTE2 搬到哪里 了,Cube 什么时候才能吃,Vector 什么时候读完,下一轮什么时候可以覆盖同一块 UB, 只要其中一个 Wait 放早了,性能掉下去,放晚了或者少了,结果直接就可能错。」 —— DeepSeek 开源昇腾基础组件将会带来哪些影响?

把这段话拆开,其实是三件互相纠缠的事。这三件事就是本章要自动化的对象。

(a) Set/Wait 配对。 昇腾的流水之间没有一条统一的栅栏可以一劳永逸,只有 HardEvent(生产者流水 × 消费者流水)上的成对 SetFlag/WaitFlag。放早了, 消费者读到旧数据;放晚了,本来能重叠的两段被串行化;漏一条,结果直接错。

(b) Cube 与 Vector 的生产者—消费者关系。 GEMM 的结果在 L0C,后处理(ReLU、 量化、softmax)要搬到 UB 交给 Vector 做,而 Cube 和 Vector 是两条独立流水。 谁等谁、等多久、中间那块 UB 什么时候能换主人,都要人写。

(c) 跨迭代复用同一块 buffer。 多缓冲不是「写一个 i % 2」那么简单。 第 0 轮还占着 slot 0 的时候第 1 轮去用 slot 1,这样下一次绕回 slot 0 才多争取了 一整轮的时间;但多一份版本就实打实多占一份 UB 或 L1。所以「开几份」和「什么时候发」 本来就是同一个问题。


1.2 为什么它可以被形式化为调度问题

上面三件事看起来是硬件知识,其实都是约束。一旦把硬件事实写成约束,剩下的就是一个 标准的约束满足 + 优化问题。同一个来源把 TileLang 的做法概括得很准确:

「它的做法也很直接,先把一次 kernel 里面的计算和搬运拆成任务,标清每个任务使用的是 MTE1、MTE2、MTE3、Cube、Vector、Fixpipe 还是 Scalar,同时把谁读谁写、谁依赖谁、 一个任务大概多久以后能产生可用结果、同一条流水隔多久还能继续接下一项任务,以及 UB、L1 这些片上空间到底还有多少容量全部放进同一个调度模型里,然后再去决定这些任务 到底应该什么时候发、哪些可以跨 iteration 重叠、一个 buffer 到底应该开一份还是两份 三份。」 —— 同上,community 级

这段话正好给出了模型的五个要素。本章后面每一节都对应其中一项:

要素 含义 本章位置 源码位置
任务(task) 一次调度里不可再分的一个执行单元 §2.1 MaterializeScheduleUnits
资源标签(pipe) 这个任务占用哪条硬件流水 §2.2 ResourcePipe
依赖边(dependency) 谁必须在谁之前、跨几轮 §2.3 dependency_analysis.cc
延迟(latency / II) 多久出结果、多久能再发一条 §2.4 EstimateLatency
容量(capacity) 片上还剩多少给多缓冲 §2.5 memory_groups

「Z3 自己当然不知道什么叫 Cube,也不知道什么叫 MTE,它只是负责解方程,真正的硬件 知识仍然来自 TileLang 对访问区域、资源占用、延迟和物理存储大小的建模。」 —— 同上,community 级

这句话是本章的题眼。编译器团队的大部分工作不是写求解器,而是把这个模型建准。 第 5 节会说明建不准的三种后果。


1.3 编译器自己怎么说:它接过了多少活

锁定版本源码里有一张对照表(资料库索引),把「用 TileLang 写」和「你自己写 Ascend C」逐项对照。其中两行正好对应本章的两半内容:

关注点 你用 TileLang 写什么 可以省掉的 Ascend C 代码 编译器替你做什么
调度与流水 T.Pipelined(...) 手写调度表、手转 buffer slot 给任务分配 AIC/AIV,安排重叠与多缓冲
同步 什么都不写 手动 Set/Wait 与 flag id 管理 推断流水与 Cube/Vector 依赖,插入配对 flag

注意「同步」那一行里 TileLang 侧写的是 None——原文是 None (Just write operations in order!)。也就是说,在默认路径下,用户按数据流顺序 写操作,同步由编译器补。这句话要成立,前提就是本章要讲的这套建模与求解。

本章不重复「多缓冲与同步是什么」(第 22 章已讲),只回答一个问题: 编译器怎么知道该在哪儿插、插几条、buffer 开几份?


2. 建模:把 kernel 变成一个调度问题

模序调度:直线依赖与跨迭代依赖直线依赖distance = -1loadA(MTE2 GM→L1)latency 190, ii 64L1→L0A(MTE1)latency 5, ii 1Cube MADlatency 8 + M·K·N/8192L0C→GM(Fixpipe)latency 200下一轮 loadAWAR:覆盖同一块 L1
同一轮迭代内的直线依赖决定任务的相位,跨迭代的 WAR 依赖(distance = -1)交给求解器用 buffer 份数解决。
  • loadA → L1→L0A → Cube MAD → L0C→GM 这一串是同一轮迭代内的直线依赖,决定任务的相位。
  • 虚线来自「下一轮 loadA」:跨迭代 WAR 依赖 distance = -1,交给求解器用 buffer 份数(ring 宽度)解决。
  • start = k × II + r:k 是任务的流水级,r 是稳态相位(0 ≤ r < II),II 是最小启动间隔。
  • 跨迭代 WAR 约束 start(下一轮) − start(本轮) ≥ 15 − II × buf:依赖跨几轮由求解器选定的 buffer 份数决定。
AutoSchedule 的输入:带标签的任务图TaskNode(每个任务带 latency / ii / pipe_mask)容量约束数据依赖 latency=190latency=5latency=200latency=58片上容量shared / shared.l1 / l0a / l0b / l0cT0 loadApipe = MTE2T1 hif8→l0pipe = MTE1T0' 下一轮同 MTE2 资源冲突T2 cubepipe = CubeT3 l0c→ubpipe = FixpipeT4 relupipe = Vector
每个 TaskNode 带 latency / ii / pipe_mask;数据依赖、资源冲突、owner-exclusion 与片上容量一起交给求解器。
  • T0′ 是下一轮的 T0:它与本轮 T0 争同一条 MTE2 流水,是一条资源冲突边(虚线)。
  • T2 与 T3 之间除了数据依赖,还有 owner-exclusion:两者共享同一块 L0C 的生命周期。
  • 依赖只有两类:kData(生产者→消费者的数据依赖)与 kOwnerExclusion(同一块可多缓冲存储的两个互斥 owner)。
  • distance 有三档:0 同迭代、-1 跨迭代未定版(交给求解器)、≥1 手工多缓冲写死的跨轮距离。
  • 资源标签只有七条流水:MTE1 / MTE2 / MTE3 / Cube / Vector / Fixpipe / Scalar。

2.1 任务:从语句到 TaskNode

调度器的输入不是「语句」,而是任务。一条语句可能被合并进任务,一个任务也可能包含 若干条必须顺序执行的语句。

MaterializeScheduleUnits(src/ascend/transform/materialize_schedule_units.cc, 文件头注释在 :18-22)负责把任务边界规范化。它做两件事:

  1. 把用户用 T.Task() / T.PerCoreTask() 显式标记的区域变成一个任务;
  2. 把其余「带依赖的叶子语句」就地推断成任务,让调度器面对的永远是整齐的任务列表。

用户侧的两个标记在 tilelang/ascend/language/schedule_hint.py:65 和 :13 定义:

  • T.Task(latency=None, ii=None):把一段顺序语句合成一个任务。文档明确写了两条 契约——「所有语句必须使用同一条昇腾硬件流水和同一个 AIC/AIV 核心」、「latency >= ii」。
  • T.PerCoreTask():把一个「逻辑上的单核任务」表达成一组互斥的候选点(例如按 block_idx 分派到不同核)。调度器把它当一个任务分析,但在插同步时会把同一对 Set/Wait 放回每一个候选访问点。

任务在 IR 里靠两个属性标记,见 src/ascend/transform/attr.h:27 与 :38:

  • tl.ascend_task:这个区域要合成一个 TaskNode;
  • tl.schedule_unit:AutoSchedule 之后、下游 pass 之间传递 stage/guard 元数据的 短命包装,不再引入任务分组语义。

编译期真正承载任务的对象是 TaskNode(ir_structure.h:494),它上面挂着 SetLatency/SetII(:330-333)、GetPipeMask(:519)、读写区域 (:533-536)。也就是说,任务这个东西不是抽象概念,就是 IR 上的一个类。

2.2 资源标签:只有七条流水

昇腾的硬件流水被抽象成一个 7 位的位掩码,定义在 src/ascend/transform/ascend_pipe.h:52-62:

位 名字 覆盖的搬运/计算
1<<0 MTE1 L1 → L0A / L1 → L0B
1<<1 MTE2 GM → UB / GM → L1
1<<2 MTE3 UB → GM / UB → L1
1<<3 Cube M 流水:MAD 矩阵乘
1<<4 Vector V 流水:SimtVF / SimdVF / 向量算术
1<<5 Fixpipe L0C → UB / L0C → GM
1<<6 Scalar S 流水:标量与配置类操作

此外还有 kAll(全 1),只用于整核 PIPE_ALL 栅栏。这个位掩码在 Z3 侧就是 resource_flags 数组,注释写在 tilelang/ascend/transform/z3_scheduler.py:87-88: MTE1=1, MTE2=2, MTE3=4, Cube=8, Vector=16, Fixpipe=32, Scalar=64。

为什么是这七个? 因为它们就是「发射端口」的粒度:一条 MTE2 和一条 Cube 可以在 同一拍各自发射,而两条 MTE2 不能——它们争用同一个端口。注意「不能同时发射」 不等于「不能同时在飞」:只要 ii < latency,同一条流水上前一条还没搬完就可以 发射下一条(这正是 §2.4 要讲的区分)。所以约束写成「同一条流水上两个任务不能并发 发射」,而不是「不能重叠」。

2.3 依赖边:两类,三种距离

依赖分析在 src/ascend/transform/auto_schedule/dependency_analysis.h 与 dependency_analysis.cc。DepInfo(dependency_analysis.h:56-75)只定义了两类 依赖:

  • kData:有向的生产者→消费者数据依赖(就是通常说的 RAW,加上写后写、写后读这类 需要保序的访问冲突);
  • kOwnerExclusion:同一块「可多缓冲的存储」被两个互不相关的 owner 使用。 这两个 owner 谁先谁后都可以,但它们的存储生命周期不能重叠。

每类依赖还带一个 distance,语义写在 dependency_analysis.h:62-73:

distance 含义
0 同一轮迭代内的依赖
-1 跨迭代依赖,尚未定版;交给调度器用 ring 宽度(buffer 份数)解决
>=1 手工多缓冲的 buffer,跨了几轮硬件依赖已经由用户写死

注意 -1 这一档:「跨几轮」不是分析出来的,是求解出来的。 这让「依赖」和 「buffer 开几份」变成了同一个未知数。

哪块存储可以被多缓冲,由 AnnotateMultiBufferEligible (src/ascend/transform/annotate_multi_buffer_eligible.cc:19-38)决定:自动推断出 「写在前、读在后、且完整覆盖一次 epoch」的最深循环作为 owner,同一存储可以有多个 owner;用户也可以用 T.annotate_buffer_versions({buf: 1}) 把它移出候选。

2.4 延迟:latency 与 II 是两件事

这是本章最容易搞错、也是原文专门点出的一处:

「这里有一个很关键的细节,就是 TileLang 会把 latency 和 issue interval 分开,因为 一条指令需要 30 个 cycle 才产生结果,并不意味着这条 Pipeline 30 个 cycle 内都不能 接下一项任务,有些流水可能每隔 8 个 cycle 就能继续 issue……」—— 同上,community 级

源码里两者是两个字段(src/ascend/transform/auto_schedule/task_annotations.h:41-42):

  • latency:从任务发射到输出可用要多少 cycle,约束的是消费者;
  • ii:同一条流水下一次发射至少要隔多少 cycle,约束的是生产者自己。

两者都必须满足 latency >= ii(task_annotations.h:149-153 会 ICHECK)。

EstimateLatency(src/ascend/transform/estimate_latency.cc)负责把这两个数算出来, 它的算法写在自己的注释里(:330-345):

  1. 访存延迟:遍历读写区域,把 (src scope, dst scope) 映射到一条物理路径, 记 base + bytes / bandwidth;
  2. 计算延迟:Cube 记 M*K*N / cube_throughput_<dtype> + base,Vector 取 「操作计数」与「bytes / valu_bandwidth」的较大者,纯标量任务记 1 cycle;
  3. II:对每条活跃流水取「带宽/吞吐限制的发射间隔」的最大值;
  4. 总延迟 = 访存延迟 + 计算延迟(顺序假设),重叠交给 Z3 用 II 处理(:941)。

这些公式里的硬件常量是源码里的默认参数,不是实测数字。例如 estimate_latency.h 里 cube_base_latency{8}、cube_throughput_fp16{8192} (ops/cycle,含乘加计两次)、mte2_gm_to_l1_base_latency{190}、 mte2_gm_to_l1_min_ii{64}、mte2_gm_to_ub_base_latency{90}、 mte3_ub_to_gm_base_latency{180}、l1_to_l0a_bandwidth{256}、 valu_bandwidth{256}。 这些数字只代表模型:它们对不对,要靠第 5.5 节的实验去校准。

2.5 片上容量:把 buffer 份数变成约束

片上空间是有限的,多开一份版本就多占一份。AutoSchedule 为每个 scope 设一个容量上限 (auto_schedule.cc:1341-1347):

unit_builder.SetMemoryLimit("shared", shared_mem_limit);          // UB
unit_builder.SetMemoryLimit("shared.l1", GetL1MemoryLimit(target));
unit_builder.SetMemoryLimit("shared.l0c", GetL0CMemoryLimit(target));
unit_builder.SetMemoryLimit("shared.l0a", GetL0AMemoryLimit(target));
unit_builder.SetMemoryLimit("shared.l0b", GetL0BMemoryLimit(target));

用户可以用 T.annotate_unlimit_memory(scope) 把某个 scope 当成无限大,见 tilelang/ascend/language/annotations.py:165。

在传给 Z3 之前,这些信息被整理成 memory_groups:每组是 [容量, buffer_idx...](z3_scheduler.py:325-330 的文档,Python 侧约束在 :431-436)。于是「多缓冲」变成一个纯算术约束:

sum(buf[i] * size[i] for i in group) <= capacity
1 <= buf[i] <= num_stages

不能多缓冲的 buffer(以及被用户钉死份数的 buffer)会直接从容量里扣掉 (auto_schedule.cc:846-857),所以如果扣完之后容量已经为负,Z3 还没开始就已经无解—— 源码会提前报错(见 §5.2)。

2.6 软件流水的表示法:start = k * II + r

最后一块拼图是「时间怎么表示」。循环里的调度器不直接解绝对时间,而是解 模序调度(modulo scheduling) 的标准形式:

start_i = k_i * II + r_i        k_i >= 0, 0 <= r_i < II
  • II 是「相邻两轮迭代的最小启动间隔」(initiation interval),也是要最小化的目标;
  • r_i 是任务在流水稳态里的相位,决定同一轮里谁先谁后;
  • k_i 是任务的流水级(stage),决定它相对第一轮被推迟了几轮。

对应的概念图(Mermaid 源见 图:modulo-schedule):

模序调度:相邻两个迭代的相位WAR:覆盖前一轮的 L1,需先读走迭代 0:loadAMTE2迭代 0:L1→L0MTE1迭代 0:Cube迭代 0:L0C→GMFixpipe迭代 1:loadAMTE2offset = II迭代 1:L1→L0MTE1迭代 1:Cube
相邻两个迭代之间错开一个 II,各自按同一串流水级推进;虚线标出跨迭代的 WAR 约束——下一轮 loadA 会覆盖上一轮还没读走的 L1。
  • 原块把 WAR 的虚线画在同一对节点(loadA → L1→L0)上,本图沿用同一画法:它表示的是跨迭代约束,不是迭代内的直线依赖
  • II 是相邻两轮迭代的最小启动间隔,也是求解器要最小化的目标;迭代 1 整体相对迭代 0 偏移 II
  • start = k × II + r:k 是任务的流水级(stage),r 是稳态相位(0 ≤ r < II)
  • 跨迭代的 WAR 靠相位解决不了,要交给求解器用 buffer 份数(ring 宽度)解决

3. 用法:你要写多少,编译器替你写多少

3.1 默认路径:什么都不用写

默认情况下 TL_ENABLE_AUTO_SCHEDULE 是打开的吗?是。tilelang/ascend/pipeline.py:22-33 里的 allow_autoschedule() 用 pass_ctx.config.get(..., True) 取值,默认 True; 关掉它只是「不调度」,而不是换一个调度器——注释原文是 disabling it leaves the kernel unscheduled。

所以下面这段改写自锁定版本 examples/ascend/example_gemm.py:60-80 的代码 (为便于阅读去掉了 unit_flag_ctrl 与 acc 分支),一行同步都没写:

with T.Kernel(NUM_BLOCKS) as bx:
    res = T.alloc_l0c((TILE_M, TILE_N), "float32")
    x_l1 = T.alloc_l1((TILE_M, TILE_K), dtype)
    w_l1 = T.alloc_l1((TILE_N, TILE_K), dtype)
    temp = T.alloc_shared((TILE_M // 2, TILE_N), out_dtype)
    for tile_idx in T.Persistent([OUT_TILES], NUM_BLOCKS, bx):
        m_tile, n_tile = aswt_swizzle(tile_idx)
        for kt in T.Pipelined(K_TILES, num_stages=NUM_STAGES):
            T.copy(X[...], x_l1)
            T.copy(W[...], w_l1)
            T.gemm(x_l1, w_l1, res, transpose_B=True, clear_accum=kt == 0)
        if MIXED:
            T.dual_copy(res, temp)
            T.dual_copy(temp, C[...])
        else:
            T.copy(res, C[...])

用户表达的是数据流(数据先去哪、算在哪、结果去哪),剩下的交给 13 个 pass。 这里的 NUM_STAGES = 2(example_gemm.py:32)就是 §4.2 里传给 Z3 的 num_stages: 它是自动 buffer 的份数上界,不是最终份数。

3.2 手动路径:调度器留了哪些旋钮

当自动结果不理想或需要精确控制时,前端提供了下面这些旋钮。全部来自 tilelang/ascend/language/schedule_hint.py 与 annotations.py,逐个在 _vendor/tilelang-v0.1.15 里核对过:

API 作用 定义位置
T.Task(latency, ii) 手工给出任务的延迟与发射间隔,覆盖估算 schedule_hint.py:65
T.PerCoreTask() 把一个逻辑单核任务写成多个互斥候选点 schedule_hint.py:13
T.Stage(k) 手工指定流水级;一旦某个子列表里出现,整个子列表转为手动调度 schedule_hint.py:118
T.annotate_buffer_versions({buf: n}) 固定/移除某个 buffer 的版本数;也可只给模式 "auto"/"iteration"/"counter" annotations.py:19
T.annotate_manual_multi_buffer(buf, {buf: n}) 声明「我已经自己多缓冲了」,只让编译器做依赖分析与插同步 annotations.py:107
T.annotate_unlimit_memory(scope) 某个 scope 视为无限大 annotations.py:165
T.assume_no_conflict(...) 告诉依赖分析两条访问不冲突 schedule_hint.py:196
T.assume_conflict(...) 强制两条访问视为冲突 schedule_hint.py:246
T.Pipelined(..., num_stages=n) 自动 buffer 的份数上界 第 11 章

几点容易被忽略的规则(都写在对应 API 的 docstring 里):

  • T.Stage 必须包住完整的调度任务,不能出现在 T.Task、T.SimtVF、T.SimdVF、 SBlock 或并行循环内部;
  • 非零 T.Stage 要求外层循环开启 annotations={"enable_offset": True}, 否则 Z3 侧会直接抛错(z3_scheduler.py:369-370);
  • 手动调度时不能再传 T.Pipelined 的 order/stage 参数——昇腾手动模式从源码顺序 推导每条流水的发射顺序(auto_schedule.cc:723-733 的 ICHECK)。

3.3 一个两种模式都成立的骨架

下面这个骨架把自动和手动放在同一段代码里(常量与省略号处的索引按真实 shape 补全; 本机只做了 API 名核对,未在 NPU 上运行;可运行的完整版本是锁定版本的 examples/ascend/example_gemm.py):

import tilelang
import tilelang.ascend.language as T


@T.prim_func
def gemm_relu(X: T.Tensor((M, K), "bfloat16"),
              W: T.Tensor((N, K), "bfloat16"),
              C: T.Tensor((M, N), "float32")):
    with T.Kernel(NUM_BLOCKS) as bx:
        a_l1 = T.alloc_l1((TILE_M, TILE_K), "bfloat16")
        b_l1 = T.alloc_l1((TILE_N, TILE_K), "bfloat16")
        c_l0c = T.alloc_l0c((TILE_M, TILE_N), "float32")
        c_ub = T.alloc_shared((TILE_M // 2, TILE_N), "float32")

        # 只给上界;真正开几份由 Z3 在容量约束下决定
        for kt in T.Pipelined(K // TILE_K, num_stages=2):
            T.copy(X[bx * TILE_M, kt * TILE_K], a_l1)
            T.copy(W[0, kt * TILE_K], b_l1)
            T.gemm(a_l1, b_l1, c_l0c, transpose_B=True, clear_accum=(kt == 0))

        T.dual_copy(c_l0c, c_ub)
        with T.SimtVF(threads=128):
            for i, j in T.Parallel(TILE_M // 2, TILE_N):
                c_ub[i, j] = T.max(c_ub[i, j], 0)
        T.copy(c_ub, C[bx * TILE_M, 0])

这段和知乎原文引用的官方 GEMM+ReLU 例子形状一致(原文给出的是同一类代码, community 级转述,未逐字核对)。它体现的设计选择是:昇腾特有的资源信息 (L1/L0C/UB)暴露在前端,但调度与同步不暴露——因为前者影响合法性,后者可以解。


4. 实现机制:13 个 pass 与一个 SMT 求解器

4.1 自动调度是一条 13 个 pass 的链

打开 _vendor/tilelang-v0.1.15/tilelang/ascend/pipeline.py,第 103-116 行就是全部:

if auto_schedule_enabled:
    mod = ascend_transform.NormalizeControlFlowForSchedule()(mod)
    mod = ascend_transform.NormalizeConflictHints()(mod)
    mod = ascend_transform.MaterializeScheduleUnits()(mod)
    mod = ascend_transform.AnnotateMultiBufferEligible()(mod)
    mod = ascend_transform.EstimateLatency()(mod)
    mod = ascend_transform.AutoSchedule()(mod)
    mod = ascend_transform.AssignCore()(mod)
    mod = ascend_transform.PrepareMultiBuffer()(mod)
    mod = ascend_transform.ResolveCore()(mod)
    mod = ascend_transform.InsertSync()(mod)
    mod = ascend_transform.MaterializeMultiBuffer()(mod)
    mod = ascend_transform.LowerScheduledTIR()(mod)
    mod = ascend_transform.RestoreWhileLoops()(mod)

每个 pass 的实现文件可以在 src/ascend/transform/ 找到。下面这张表是 code/pass_order/ascend_pass_order.py 在本机跑出来的实际结果(脚本从 pipeline.py 抽顺序,再找 refl::GlobalDef().def("tl.transform.<Pass>")):

# pass 做什么 实现(src/ascend/transform/)
1 NormalizeControlFlowForSchedule 把 while 改写成有界 for,抽出复杂条件,让循环体能被调度 normalize_control_flow_for_schedule.cc:240
2 NormalizeConflictHints 规范化 T.assume_no_conflict/assume_conflict normalize_conflict_hints.cc:309
3 MaterializeScheduleUnits 规范化任务边界,物化平凡调度单元 materialize_schedule_units.cc:578
4 AnnotateMultiBufferEligible 标出每个循环里哪些片上 buffer 可以被多缓冲 annotate_multi_buffer_eligible.cc:835
5 EstimateLatency 给每个任务标 latency 与 II estimate_latency.cc:2271
6 AutoSchedule 调 Z3 求调度解(唯一一次求解) auto_schedule.cc:1400
7 AssignCore 给任务标最宽的合法 AIV/AIC mask assign_core.cc:401
8 PrepareMultiBuffer 选多缓冲时钟,插入 counter 的 init/advance 任务 prepare_multi_buffer.cc:1249
9 ResolveCore 按真实消费者收窄核候选 resolve_core.cc:559
10 InsertSync 插同步:算 hard_event、分配 flag、生成 Set/Wait insert_sync.cc:2568
11 MaterializeMultiBuffer 把逻辑多缓冲访问降到物理 buffer 版本 materialize_multi_buffer.cc:561
12 LowerScheduledTIR 把调度后的 TIR 降成 AIV/AIC 的 body lower_scheduled_tir.cc:744
13 RestoreWhileLoops 把 synthetic_while 还原成 while(true) restore_while_loops.cc:117

只有第 6 个 pass 真正在求解。它前后都是「准备模型」与「落地结果」。这也解释了 为什么第 5 节说「加能力 = 改模型」:想影响调度结果,改 EstimateLatency 或 dependency_analysis,比改 Z3 调用有用得多。

注意 pass 3-5 的顺序是硬约束:AnnotateMultiBufferEligible 的 docstring 明说它要求 先跑 NormalizeControlFlowForSchedule 和 MaterializeScheduleUnits (tilelang/ascend/transform/__init__.py:31-37),否则 buffer 相关的循环边界还不是 可调度的任务,写先读后的判断就不可靠。

4.2 Z3 到底解的是什么

先看事实:求解器用 Python 写的,通过 TVM FFI 被 C++ 调用。C++ 侧在 auto_schedule.cc:474-479 用 ffi::Function::GetGlobal("tl.transform.z3_schedule_python") 找函数;Python 侧在 z3_scheduler.py:229 与 :785 用 @tvm_ffi.register_global_func 注册。两个入口分工不同:

入口 用途 定义
z3_schedule_python 直线(无循环)任务表:最小化 makespan z3_scheduler.py:67-226
z3_schedule_loop_python 循环体:最小化 II,同时解 buffer 份数 z3_scheduler.py:268-783

循环版是本章的重点。它的变量只有四类(z3_scheduler.py:412-420):

k[i]   : Int   —— 任务 i 的流水级(stage)
r[i]   : Int   —— 任务 i 在稳态里的相位,0 <= r[i] < II
buf[j] : Int   —— 第 j 个自动 buffer 的物理份数,1 <= buf[j] <= num_stages
O_i_j  : Bool  —— 任务 i 与 j 谁先发(只在同流水冲突时创建)
start[i] = k[i] * II + r[i]

约束一共五类。前三类是硬件事实,后两类是调度策略:

(1) 数据依赖(z3_scheduler.py:442-455)。给定 distance >= 0:

start[v] - start[u] >= latency - II * distance

distance < 0 时,II * distance 换成 II * buf[-distance-1]——依赖跨了几轮, 由求解器挑的那个 buffer 份数决定。这一行就是「多缓冲必须和调度一起算」的全部秘密。

(2) 资源冲突(:456-468)。同一流水上的一对任务二选一,并且要保护绕回 (因为模序是环形的,r 在 II 处折返):

o_ij  ->  r[j] - r[i] >= ii_i  且  r[i] - r[j] + II >= ii_j
¬o_ij ->  r[i] - r[j] >= ii_j  且  r[j] - r[i] + II >= ii_i

(3) 片上容量(:431-436):

sum(buf[j] * size[j] for j in group) <= capacity[group]
1 <= buf[j] <= num_stages

(4) owner-exclusion(:472-488)。两个不相关 owner 共用同一段物理 ring 时, 选一个模序,并同时保护正向交接和绕回交接(注释原文: protect both the forward hand-off and the wrap-around hand-off)。

(5) 策略约束。两条容易忽略:

  • stage_order_deps(:489-499)在拷贝 Let 变量时要求 k_u <= k_w,保证被克隆的 Let 读到的是写之前的旧值;
  • enable_offset(:503-514)。默认 False 时,每条流水上的任务必须落在同一个 II 窗口内(max(start) - min(start) < II),即不允许把一条流水的任务摊到多个流水级; 置 True 才解开。手写 T.Stage 的非零流水级要求打开它(见 §3.2)。

目标:最小化 II。实现方式是对 II 做二分(:398-540),而不是让求解器直接 优化 II——因为约束里 r[i] < II 和 II * distance 都以 II 为参数,把它当常数后 每个子问题都是纯可行性问题。二分范围是 [max(ii_i), max(sum(latency), sum(ii)) + 1]。

三个工程细节值得记住:

  • 每次求解用私有 z3.Context()(:391 与 :132)。注释直接写了原因:共用全局 context 会让「重复的同一个求解」返回不同的(都合法的)模型,导致编译结果不可复现;
  • Z3 不是线程安全的,Python 侧用一把全局锁串行化(:17 的 _z3_lock);
  • rlimit = 1000000(:415)限制求解步数,seed = 42(FFI 默认, :849)固定随机性。

4.3 最小可复现的玩具模型

这一段的目标是:把上面的约束缩到你能用手算验证的规模。 code/toy_scheduler/toy_schedule.py 用纯 Python 实现了同一套约束(不用 Z3, 用回溯搜索),跑一个 4 任务的 GEMM 流水:

任务:
  0  loadA(MTE2 GM->L1)   latency=40  ii=10  pipe=MTE2
  1  l1->l0(MTE1)         latency=15  ii= 3  pipe=MTE1
  2  cube(Cube)           latency=30  ii= 8  pipe=Cube
  3  l0c->gm(Fixpipe)     latency=25  ii= 6  pipe=Fixpipe

同迭代真依赖:  0 -> 1 (>=40)   1 -> 2 (>=15)   2 -> 3 (>=30)
跨迭代 WAR:    1 -> 0,distance = -1(自动 buffer 0)
资源冲突:      (2, 1)(Cube 与 MTE1 争 L0 端口)
容量:          L1 上 buffer 0 一份 1000 字节,容量 4096,num_stages = 2

约束就是 §4.2 的五类去掉 owner-exclusion。手推的关键只有两条:

  • 真依赖要求 start[1] >= start[0] + 40;取 start[0] = 0,即 start[1] >= 40;
  • 跨迭代 WAR 是 start[0] - start[1] >= 15 - II * buf[0](distance = -1), 代入 start[0] = 0 得 start[1] <= II * buf[0] - 15。

两式相消:II * buf[0] >= 55。所以

  • buf[0] = 1 → II >= 55(不重叠,退化成串行);
  • buf[0] = 2 → II >= 28(L1 占用 2000 <= 4096,装得下)。

程序实测结果(本机 python3 toy_schedule.py demo,完整输出见 资料库索引):

first feasible II = 28, buffer versions = [2]
    [  0] loadA(MTE2 GM->L1)   k=0 r= 0 latency=40 ii=10 pipe=MTE2
    [ 40] l1->l0(MTE1)         k=1 r=12 latency=15 ii= 3 pipe=MTE1
    [ 55] cube(Cube)           k=1 r=27 latency=30 ii= 8 pipe=Cube
    [ 85] l0c->gm(Fixpipe)     k=3 r= 1 latency=25 ii= 6 pipe=Fixpipe

它和真实 Z3 求解器的差别只有两点,必须说清楚:

  1. 求解器不同。玩具用回溯搜索;Z3 用 SMT。对 4 个任务的实例两者都精确,但 「回溯能解」不证明「Z3 会给出同样的解」——Z3 只保证在给定目标下最优,不保证 唯一(这也是源码要用私有 context 固定随机性的原因)。
  2. 少了 owner-exclusion。玩具模型里每个 buffer 只有一个 owner。

python3 toy_schedule.py --print-smt 会把同一实例写成 z3 API 的约束文本; 装了 z3-solver 的机器上 --z3 会真正复算并比对(本机没有 z3,未验证)。

4.4 求解结果怎么落地:从 start_times 到 Set/Wait

Z3 只回答「这样排在数学上成立」。把它变成程序要走四步。

第 1 步:写回 stage / start / buffer 份数(pass 6)。 auto_schedule.cc:1137-1144 从 start_times[i] / II 反推出每个任务的 stage, 并把 start_time 归一化到从 0 开始:

nodes[i]->SetStage(manual_schedule
                       ? static_cast<int>(manual_stages[i])
                       : (enable_offset ? (raw_k[i] - min_k_raw) : 0));
GetAutoScheduleExtraInfo(nodes[i])->SetStartTime(start_times[i] - min_start_time);

紧接着 :1284-1295 把每个 storage 的版本数合并进 selected_buffer_versions_ (MergeSelectedBufferVersion,定义在 :245)。注意 :251-257 的注释坦承了一个 已知限制:两个 owner 的版本数取 max 之后不再复核物理上限,可能产出一个 超过片上容量的合并分配,「until a lifetime-aware global check exists, this is an accepted limitation」——只能靠下游或 runtime 报错。这是本章第 5 节要讲的风险之一。

第 2 步:核分配与多缓冲协议(pass 7、8、9)。 AssignCore(assign_core.cc:401)先给每个任务一个「最宽的合法 AIV/AIC mask」, PrepareMultiBuffer(prepare_multi_buffer.cc:1249)选多缓冲时钟并插入 counter 的初始化与推进任务,ResolveCore(resolve_core.cc:559)再根据真实消费者把 mask 收窄。分成三步是因为「能放在哪个核」和「最终放在哪个核」是两件事:插入 counter 会 产生新的标量消费者,必须等它们都出现之后才能定核。

第 3 步:插同步(pass 10)。 InsertSync(insert_sync.cc:2568)是这一章里 代码量最大的 pass(约 104 KB)。它做三件事:

  1. 由「生产者流水 × 消费者流水」算出 HardEvent 名字,GetHardEvent 在 :751, 例如 MTE2 + Cube 要归一化成 MTE2_MTE1(因为 Cube 从 L0 取数,前面实际是 MTE1);
  2. 给每个同步点分配 flag id,并把 event id 与版本号绑起来 (MakeFlagEventId,:2299);
  3. 生成 IR:MakeAscendSetFlag / MakeAscendWaitFlag(:2270-2280)产出 Evaluate(Call(ascend_set_flag(), {StringImm(hard_event), event_id}))。

跨核(AIC↔AIV)另有一对 ascend_cross_core_set_flag / wait_flag (:2287-2297),它们在 :2391-2414 处按 mode 组合成正式同步。

第 4 步:物化 buffer 版本与最终 body(pass 11、12、13)。 MaterializeMultiBuffer(materialize_multi_buffer.cc:561)把逻辑访问 buf[...] 改写成 buf[version, ...]——注意它连 buffer 的存储 stride 都一起算 (BufferStoragePitch,:56),所以所有 alias view 会看到同一份物理 版本。LowerScheduledTIR(lower_scheduled_tir.cc:744)再把调度后的 TIR 降成 AIV/AIC 各自的 body,RestoreWhileLoops 收尾。

于是整条链可以浓缩成一句话:

AutoSchedule 写回之后的 pass 链II / stage / start(K, r)AutoSchedule 写回buffer 份数 → PrepareMultiBuffercounter init/advance依赖 + 流水资源 → InsertSyncset_flag / wait_flag(hard_event, event_id)MaterializeMultiBuffer → buf[version, ...]LowerScheduledTIR → AIV / AIC body
AutoSchedule 把求解出的 II、stage 与相位写回后,整条链依次落到 buffer 份数、同步插入与多缓冲物化,最后 lower 成 AIV/AIC 各自的 body。
  • 链上的每一步都是昇腾 transform 里的一个 pass,顺序与注册点一致
  • MaterializeMultiBuffer 把 buf[...] 改写成 buf[version, ...],连存储 stride 一起算,之后才是 LowerScheduledTIR

在 AutoSchedule 之后的 pass 里,还有两个只和「资源不够分」有关的 pass 值得点名: MergeUBAllocations 做片上存储复用合并;RewriteFlagToBuf 把每个 hard_event 归一到 8 个 flag 槽,装不下时用 0/1 背包(容量 8)挑一部分,其余溢出到共享的 32 槽 get_buf/rls_buf 互斥池(rewrite_flag_to_buf.cc:3-14)。 这两条都说明:昇腾侧连「flag 编号」都是需要分配的资源。

4.5 为什么 CUDA 侧不需要这一步

同样一个 TileLang 前端,CUDA 后端里没有 Z3、没有 AutoSchedule、没有「求解器」。 打开 tilelang/cuda/pipeline.py 能看到对应位置是什么:

# tilelang/cuda/pipeline.py:129-130
mod = tilelang.transform.PipelinePlanning()(mod)
mod = tilelang.transform.InjectSoftwarePipeline()(mod)
...
# tilelang/cuda/pipeline.py:258-259
mod = tilelang.transform.ThreadSync("shared")(mod)
mod = tilelang.transform.ThreadSync("shared.dyn")(mod)

三个差异,都不是「NV 更简单」,而是模型不同:

差异一:多缓冲份数在 CUDA 上是「算出来的」,不是「解出来的」。 src/transform/inject_pipeline.cc:1498 起,版本数是一条结构化的上界:

// `use - def + 1` is a upper bound of the needed versions
int num_versions = buffer_info.use - buffer_info.def + 1;

再加一个 num_versions == 2 的特例判断(:1504-1546)。推导依据是 PipelinePlanning 已经分配好的 stage 与 order,不存在「容量装不下要退几份」的 联合优化——因为 CUDA 的流水级由用户/autotuner 的 num_stages 直接决定, shared memory 的份数只影响占用,不影响合法性。

差异二:CUDA 的「自动调度」是固定角色启发式,不是求解。 src/cuda/transform/auto_schedule/role_based_scheduler.h:17-22 的注释写的是 Fixed-role heuristic:按 lowering 资格把算子分成 Load / MMA / Store / Worker, 把 warp-private 的 def-use 链拉进消费者的角色,再给每个跨角色传递的 shared/tensor memory 存储做流水。角色是分类出来的,不是求解出来的。

差异三(最根本):硬件同步语义不同。 这部分来自知乎那张对照表(community 级)与第 6 章的结论:

维度 NVIDIA GPU 昇腾 950
数据传递 普通访存 + cp.async/TMA 等异步机制 不同存储层级通过指定搬运路径连接
同步 线程、线程组、异步操作各自的完成协议 需要处理流水线间、Cube/Vector 间、多缓冲复用的完成协议

具体到 TileLang 的实现差异,可以归纳成三句话:

  1. CUDA 侧的同步粒度是「块」:ThreadSync 只需在 block 内插 barrier 语义 (tvm_storage_sync),而 block 内的 shared memory 可见性由硬件 barrier 保证。 所以在 TileLang 的 CUDA 路径里,同步一旦插入就是保守但正确的:粒度粗一点只是慢。
  2. 昇腾侧的同步粒度是「流水对 + flag 槽」:HardEvent 有生产者、消费者两个维度, flag id 还是有限资源,所以「插哪里」和「用哪个编号」都是组合问题。
  3. 昇腾侧不存在统一栅栏可用:用一条全核栅栏换正确性会把所有重叠都杀掉, 这正是第 1.1 节说的「放晚了性能掉下去」。

反过来说,如果昇腾只有一种执行单元、或者只有一条流水,那么用保守栅栏就够了, 也就不需要 Z3。需要求解,是因为硬件给了 7 条可独立发射的流水和好几块容量不同的 片上存储,而正确性只能靠显式 flag 保证。


5. 进阶与陷阱

5.1 模型不准:从「慢」到「错」

模型里的每一个数字都是一个假设。假设偏小(估得比真实快)会让消费者提前启动, 结果是错的;假设偏大(估得比真实慢)只是把机器排得更保守,结果是慢的。

这不是空谈。code/toy_scheduler/toy_schedule.py 的 sensitivity 模式就是这件事的 可复现版本(本机实测):

== 真值 ==  MTE2 latency = 40  ->  II = 28,buffer versions = [2]
== 低估后 == EstimateLatency 报 16 -> II = 16,buffer versions = [2]
== 用真值复核 ==
  违约:l1->l0(MTE1) 比 loadA(MTE2 GM->L1) 早了 24 cycles(数据未就绪)

也就是说,一个 24 cycle 的估计误差,就足以让求解器给出一个在真实硬件上非法的 时间表。真实系统里对应的风险点有:

  • estimate_latency.h 里的带宽/吞吐常量是按芯片世代手工标定的(如 cube_throughput_fp16{8192}、mte2_gm_to_ub_bandwidth{100})。换芯片、换 dtype、换 hf32 模式都会改变真值,但表不会自动跟着变;
  • EstimateLatency 的总延迟是 memory_latency + compute_latency(estimate_latency.cc:941), 这是一个顺序假设;真实硬件上不同路径之间的重叠比这个假设好,所以估算偏保守;
  • 未知操作走 default_operation_throughput{0} 的「1 cycle 兜底」 (estimate_latency.h),这属于偏小的估计——正好是危险的方向。

所以「性能次优」和「结果错误」在同一个根因下:模型。这也是为什么原文说 「加能力 = 改模型」。

5.2 求解失败的四种形态

AutoSchedule 是 fail-fast 与静默降级的混合体。列出四种,按「可诊断性」从好到坏:

(1) 容量先天不足 → 直接 FATAL。 auto_schedule.cc:866-891 在调用 Z3 之前先检查: scope 容量为负,或「每个可多缓冲 buffer 至少一份」已经超过剩余容量,就报 Z3 loop scheduling failed: scope "..." capacity ...,并提示用 T.annotate_unlimit_memory(scope) 或减小 buffer。这类最好查。

(2) 手动调度不可行 → 抛异常。 z3_scheduler.py:534-537:手动模式下如果找不到 任何可行 II,会 raise RuntimeError("Manual schedule constraints are infeasible; ...") 并提示检查每条流水的源码顺序、T.Stage 取值、依赖和资源占用。这是对的—— 手动模式是用户契约,不能偷偷改。

(3) 自动模式不可行 → 静默退化成串行。 这是最需要警惕的一条。 z3_scheduler.py:539-550:

No feasible II found; falling back to sequential schedule
start_times[i] = start_times[i-1] + latencies[i-1]
best_ii = max(sum(latencies), 1)
buffer_versions = [1] * len(buffer_sizes)

注意这段只在 verbose=True 时打印。默认 FFI 路径不传 verbose (z3_scheduler.py:785-854),所以编译会成功,程序会正确,但性能可能塌回完全不重叠。 如果某天你发现「加了 num_stages 却一点没变快」,先怀疑掉进了这个分支。

(4) 求解被 rlimit 截断 → 可能被当成不可行。 每个子问题设 rlimit = 1000000 (:415)。代码的判断是 if solver.check() == z3.sat: ... else: ii_lower = ii_mid + 1 (:518-532),也就是说 unknown 会被当成 infeasible,然后 II 被推大。 这是一个从代码读出来的推理风险,本章未实测(本机无 z3)。任务数多、依赖稠密时, 它可能让调度器给出一个偏保守的 II,或者落到形态 (3)。

5.3 启发式与用户契约:两个会「悄悄错」的地方

求解器是精确的,喂给它的东西不是。两处需要特别注意:

(a) 多缓冲资格是启发式推断的。 AnnotateMultiBufferEligible 通过「写先读后、 完整覆盖一个 epoch」来找 owner。它的 docstring 提醒:当「自动推断漏掉了对旧数据的 读取」时,用户要用 T.annotate_buffer_versions({buf: 1}) 把该 storage 从多缓冲候选里 移除(annotations.py:41-46)。反过来说,推断错了会直接产出错误的多缓冲, 而这类错误通常只在特定 shape / 特定迭代 / 流水压得很紧时暴露——正是原文说的 「这种 bug 基本属于查起来最痛苦的那一类」。

(b) T.assume_no_conflict 是在删约束。 它的 docstring 明说:动态索引 (例如 state_cache[perm[idx], ...])导致依赖分析无法证明不重叠,于是插入冗余同步, 这个 hint 就是让用户声明「它们真的不重叠」(schedule_hint.py:196-244)。声明错了, 依赖被删掉,程序并行到了不该并行的地方——这是用户自己签字的正确性契约。 保守的做法是:只在能用数学证明(比如 perm 是置换)时才写。

两个 T.Task(latency, ii) 的坑也顺便记一下:给出的值是精确覆盖,不是 hint (schedule_hint.py:65-115);而且会被 ICHECK 校验 latency >= ii。写错方向 (ii 给大、latency 给小)等于在 5.1 的坑里自己再挖一层。

5.4 版本与状态:这一章的东西在哪个仓库

这一节的所有 pass、求解器、T.Task/T.Stage API 都在主干仓库 _vendor/tilelang-v0.1.15/ 里。核对方式(本章实际执行过):

ls _vendor/tilelang-v0.1.15/tilelang/ascend/transform/   # z3_scheduler.py 在这里
ls _vendor/tilelang-v0.1.15/src/ascend/transform/        # 13 个 pass 的 .cc 在这里
python3 code/pass_order/ascend_pass_order.py             # 顺序与注册点一次性核对

而适配器仓库 _vendor/tilelang-ascend-v0.1.2.000/ 里没有 AutoSchedule:

grep -rl "AutoSchedule" _vendor/tilelang-ascend-v0.1.2.000/src _vendor/tilelang-ascend-v0.1.2.000/tilelang
# 无输出(本机 2026-10-01 实测)

这与 版本锁定 的结论一致:昇腾后端已经进主干。你在适配器仓库里能拿到的 是文档与例子,例如《TileLang-Ascend Programming Guide》第 4.1.4.2 节讲 T.Pipelined 的 intra-core / inter-core 两种用法。

另外有两个 pass config 键在 v0.1.15 里找不到,只出现在适配器仓库的文档与 skill 说明中:tl.ascend_auto_cv_combine 与 tl.ascend_auto_cross_core_sync (适配器文档要求「使用核间流水线时必须开启」,见 tilelang-ascend/docs/TileLang-Ascend Programming Guide.md 第 1201 行)。 它们在 _vendor/tilelang-v0.1.15/tilelang/transform/pass_config.py 里搜不到 (本章实测)。待核:这属于「文档描述的旧配置面 vs 主干现状」的差异, 按 版本锁定 的规则,本书只以主干为准,本节引用的跨核同步语义请以 InsertSync 的 ascend_cross_core_set_flag/wait_flag(insert_sync.cc:2287-2297) 为准。

5.5 在有 NPU 的机器上怎么验证

本章所有结论都能用下面这套实验流程检验。写成「变量 → 观察量 → 判据」的形式, 方便你改造成回归脚本。这套流程本章一次也没跑过(无 NPU、无 CANN)。

准备。 入口用锁定版本的 examples/ascend/example_gemm.py(T.Persistent + T.Pipelined(num_stages=NUM_STAGES)),它的 benchmark 走 do_bench(..., backend="msprof", _n_warmup=30, _n_repeat=repeats) (example_gemm.py:98),性能口径是 TFLOP/s。

实验 A:多缓冲份数到底开了几份。

  • 变量:把 example_gemm.py:32 的 NUM_STAGES 从 2 改成 1/3/4;
  • 观察量:每个大 shape 的 TFLOP/s;以及 T.annotate_buffer_versions 显式钉死份数 (如 {x_l1: 2, w_l1: 2})后的对比;
  • 判据:如果 num_stages 从 2 加到 4 而性能不变,先怀疑 §5.2 的形态 (3)(静默退化) 或容量约束把版本数压回了 1。

实验 B:看求解器实际给出的计划。

  • 变量:直接调用 tilelang.ascend.transform.z3_scheduler.z3_schedule_loop_python, 传 verbose=True;
  • 观察量:控制台会打印每个任务的 start_time / latency / II / resource_flags, 并在 debug/schedule_NNN/schedule_info.json 写入完整输入输出, 同时尝试画一张时间线图(z3_scheduler.py:602-780);
  • 判据:把 minimal_II 与 max(ii_i)、sum(latencies) 三者比较——如果 minimal_II == sum(latencies),说明退化成了串行,需要查依赖是否过保守 (例如该写 T.assume_no_conflict 的地方没写)。

实验 C:看 IR 里的 buffer 版本与 Set/Wait。

  • 变量:打开 IR dump——PassConfigKey.TL_ENABLE_DUMP_IR = "tl.enable_dump_ir" 与 TL_DUMP_IR_DIR = "tl.dump_ir_path"(tilelang/transform/pass_config.py:338-342);
  • 观察量:AutoSchedule 之后、InsertSync 之后的 IR 差异:新出现的 ascend_set_flag / ascend_wait_flag 调用、tl.multi_buffer 相关的 buffer 版本 维度、以及 bind 出来的 counter 变量;
  • 判据:每个 producer→consumer 的 RAW 边都应有配对的 Set/Wait;buf 的第一维应等于 求解器给出的版本数。

实验 D:没有 NPU 也能做的一步——camodel 仿真。

适配器文档第 2.4 节说明:装了含 A5 simulator 镜像(Ascend950PR_9599)的 CANN 包后, 可以在任意 x86/Linux 机器上用 camodel(cycle-accurate model) 跑 kernel, 并输出每个 AIC core 的指令日志(core*.instr_log.dump)与 UB 读写日志 (core*.ub.rd_log.dump / core*.ub.wr_log.dump) (_vendor/tilelang-ascend-v0.1.2.000/docs/TileLang-Ascend Programming Guide.md:194-232)。 这意味着即使手上只有 x86 服务器没有 950,也能验证「Set/Wait 放的位置」是否与 求解器的计划一致——这是本章最值得做的一步验证。

实验 E:故意把模型弄错,看它怎么坏。

  • 变量:临时把 estimate_latency.h 里 mte2_gm_to_l1_base_latency 改小;
  • 观察量:正确性(与 torch_npu 参考值比对)与性能;
  • 判据:如果只是变慢,说明下游 InsertSync 兜住了;如果数值出错,说明求解器真的 把消费者提前了——这正是 §5.1 在真实机器上的版本。这个实验会改 _vendor/ 之外 的源码树,做之前先备份。

6. 本章小结

一句话:昇腾后端把「什么时候发、开几份 buffer、在哪儿插 Set/Wait」这三件事 建模成一个模序调度问题,用 Z3 求最小 II,再用一串 pass 把解落成真实的 IR。

复习卡片:

  1. 问题:Ascend C 里 MTE/Cube/Vector 是独立流水,同步靠成对的 Set/Wait, 多缓冲靠人肉算时间表;放早了慢、放晚了或漏了直接错。
  2. 建模五要素:任务(TaskNode)、资源标签(7 位 ResourcePipe)、依赖边 (kData / kOwnerExclusion,distance 有 0 / -1 / ≥1 三档)、延迟 (latency 与 ii 分开)、容量(memory_groups)。
  3. 求解:start = k * II + r;变量是 k[i]、r[i]、buf[j]、O_i_j; 约束是数据依赖、资源冲突(含绕回)、容量、owner-exclusion、stage 顺序; 目标是对 II 做二分取最小可行值。distance < 0 的那条依赖把「buffer 开几份」 耦合进了同一个问题。
  4. 落地:AutoSchedule(写回 stage/start/版本数)→ AssignCore → PrepareMultiBuffer(counter init/advance)→ ResolveCore → InsertSync (set_flag/wait_flag + flag 分配)→ MaterializeMultiBuffer(buf[version,...]) → LowerScheduledTIR;后段还有 MergeUBAllocations 与 RewriteFlagToBuf (8 槽 flag + 32 槽互斥池)。
  5. 为什么 NV 不用:CUDA 的版本数是结构公式 use - def + 1,自动调度是 固定角色启发式,同步可以靠块级 barrier 兜底;昇腾是 7 条独立流水 + 显式 flag + 多条容量不同的片上存储,所以既需要求解,也需要分配。
  6. 风险:模型估小了会错、估大了只是慢;自动模式求解失败会静默退化成 串行;assume_no_conflict 是用户签字的正确性契约;合并后的分配可能超过物理上限 (源码注释里的 accepted limitation)。

延伸阅读

习题

12 题 · 答案在提交后展开

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

E1综合难度 ★无需硬件,[无卡] 用你自己的话说明:为什么 Ascend C 里「Set/Wait 配对」

用你自己的话说明:为什么 Ascend C 里「Set/Wait 配对」 「Cube/Vector 生产消费」「跨迭代复用同一块 UB」这三件事可以合起来写成一个调度问题? 请明确指出哪一部分是约束、哪一部分是目标。

E2综合难度 ★无需硬件,[无卡] `src/ascend/transform/ascend_pipe.h` 把硬件流水定义成 7 位掩码。

src/ascend/transform/ascend_pipe.h 把硬件流水定义成 7 位掩码。 回答:

  1. 写出这 7 个名字与对应位值;
  2. 为什么 MTE2 同时负责 GM→UB 和 GM→L1 却只有一位?
  3. kAll 为什么不能出现在任务的 pipe_mask 里?
E3综合难度 ★无需硬件,[无卡] 解释 `latency` 与 `ii` 的区别。分别给出「把两者混为一谈」的

解释 latency 与 ii 的区别。分别给出「把两者混为一谈」的 两种错误方向,以及每种方向造成的后果。

E4综合难度 ★★无需硬件,[无卡] 在 `z3_scheduler.py` 的循环模型里,`data_deps` 的

在 z3_scheduler.py 的循环模型里,data_deps 的 distance 取负值时代表什么?为什么这个设计使得「buffer 开几份」不能作为调度之后的 独立步骤来处理?请用一句话概括,并说明 distance 取 0、-1、≥1 三种情况的区别。

代码阅读题

E5综合难度 ★★无需硬件,[无卡] 阅读 `_vendor/tilelang-v0.1.15/src/ascend/transform/insert_sync.cc`

阅读 _vendor/tilelang-v0.1.15/src/ascend/transform/insert_sync.cc 里的 GetHardEvent(约 :751)。回答:

  1. producer_pipe = "MTE2"、consumer_pipe = "M" 时它返回什么?
  2. 为什么要把 M 映射成 MTE1,而不是直接产出 MTE2_M?
  3. 顺着 MakeAscendSetFlag/MakeAscendWaitFlag(约 :2270)说明这对结果最终变成 什么形式的 IR。
E6综合难度 ★★无需硬件,[无卡] 在 `_vendor/tilelang-v0.1.15/tilelang/ascend/transform/z3_scheduler.py`

在 _vendor/tilelang-v0.1.15/tilelang/ascend/transform/z3_scheduler.py 的 z3_schedule_loop_python 里找到「自动 buffer 决定跨迭代距离」的那几行,给出 行号与表达式,并解释 buffer_var_id = -distance - 1 这一行的作用。

E7综合难度 ★★无需硬件,[无卡] `enable_offset` 默认是 `False`。请说明这个默认值具体加了什么

enable_offset 默认是 False。请说明这个默认值具体加了什么 约束(写出不等式),并回答:为什么手写非零 T.Stage 时必须把它打开?如果关不掉又 硬写非零 stage,代码会怎么失败?

E8综合难度 ★★无需硬件,[无卡] 阅读 `_vendor/tilelang-v0.1.15/src/ascend/transform/auto_schedule.cc`

阅读 _vendor/tilelang-v0.1.15/src/ascend/transform/auto_schedule.cc 中 MergeSelectedBufferVersion(:245)及函数内的注释(:251-256)。回答:

  1. 两个 owner 共用一段物理 ring 时,合并后的版本数取什么?
  2. 注释里承认了什么限制?这个限制可能导致什么后果?由谁来兜底?

动手题

E9综合难度 ★★无需硬件,[无卡] 运行本章的 `code/pass_order/ascend_pass_order.py`。

运行本章的 code/pass_order/ascend_pass_order.py。

  1. 贴出输出,数出 if auto_schedule_enabled: 块里有几个 pass;
  2. 找出其中唯一真正调用求解器的那个,说明它前后两个 pass 各自的职责;
  3. 假设把 AnnotateMultiBufferEligible 与 MaterializeScheduleUnits 的顺序对调, 依据这两个 pass 的 docstring 推断会发生什么,并说明你的判断依据。
E10综合难度 ★★无需硬件,[无卡] 修改 `code/toy_scheduler/toy_schedule.py` 里玩具实例的

修改 code/toy_scheduler/toy_schedule.py 里玩具实例的 memory_groups 容量,从 4096 改成 1500,然后运行 demo。

  1. 先不运行,用 §4.3 的手推方法预测 II 与 buffer versions;
  2. 再运行验证;
  3. 解释为什么求解器不能「先挑大 II、再多开版本」来救这个实例。
E11综合难度 ★★★无需硬件,[无卡] 玩具模型目前没有 owner-exclusion。请为

玩具模型目前没有 owner-exclusion。请为 「两个不相关 owner A、B 共用同一段物理 ring」补上这个约束:

  1. 写出 A before B 时要加的两条不等式(正向交接 + 绕回交接),说明每一条保护的是 哪一对访问;
  2. 解释为什么它必须写成二选一(用布尔变量),而不是一个固定的顺序;
  3. 说明为什么 z3_scheduler 的注释强调「pipeline stage offsets 故意不参与」。
E12设计难度 ★★★需要加速器,[需 NPU] 设计一个实验,用来在真实昇腾机器上判定

设计一个实验,用来在真实昇腾机器上判定 「自动求解失败 → 静默退化成串行」是否发生(§5.2 形态 (3))。要求:

  1. 给出自变量(你要改什么)与因变量(你测什么);
  2. 给出至少两条互相独立的证据(一条来自生成代码/IR,一条来自性能);
  3. 说明如果只是「性能持平」还能用什么手段排除「容量约束把版本数压回 1」这个干扰项。

章节自测

7 题 · 提交后本地评分

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

  1. 1. 本章把昇腾 kernel 的排程建模成调度问题,模型包含哪五个要素?

  2. 2. 关于 latency 与 ii(issue interval)的区别,下列哪一项正确?

  3. 3. 依赖边上的 distance = −1 表示什么?

  4. 4. 循环版 Z3 求解器解的是什么?

  5. 5. 关于这条 13 个 pass 的自动调度链,下列哪一项正确?

  6. 6. 自动模式下找不到任何可行的 II,会发生什么?

  7. 7. 为什么说「模型不准」会让结果从慢变成错?

本章代码

5 个文件

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

浏览本章全部代码 →

本章引用

源码链接指向锁定 tag

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

展开引用清单

第 23 章 参考资料

本章所有结论都基于下表的本地资料或锁定版本源码。版本以 版本锁定 为准:TileLang v0.1.15 (commit a35f8ddf45eba16c21211ec8822d56ce5363036f)、TileLang-Ascend TileLang-ascend-v0.1.2.000-release(commit 89a9364e47a341fffdd65ed94786b1d4f9b6a78c)。 文中不使用任何含糊的版本说法;凡涉及版本处一律写具体 commit 或 tag。

写作环境:macOS (Apple Silicon)、Python 3.11.7;没有 NVIDIA GPU,没有昇腾 NPU, 没有安装 z3-solver。因此本章没有任何本机实测的性能数字。


锁定的源码引用(_vendor/)

id 引用位置 用途 核验方式与状态
S1 _vendor/tilelang-v0.1.15/tilelang/ascend/pipeline.py:22-33 默认开启 AutoSchedule(TL_ENABLE_AUTO_SCHEDULE,默认 True),关掉只是「不调度」 已核验(2026-10-01):逐行阅读
S2 _vendor/tilelang-v0.1.15/tilelang/ascend/pipeline.py:103-116 自动调度 13 个 pass 的顺序 已核验(2026-10-01):逐行阅读,并由 code/pass_order/ 脚本复核
S3 _vendor/tilelang-v0.1.15/tilelang/ascend/transform/__init__.py:31,47,52,77,82,87,92,97,102,210 各 pass 的 Python 门面与 docstring;AnnotateMultiBufferEligible 对前序 pass 的依赖;RewriteFlagToBuf 的 8 槽 flag / 32 槽池 已核验(2026-10-01):逐行阅读
S4 _vendor/tilelang-v0.1.15/tilelang/ascend/transform/z3_scheduler.py:67-226, 268-783 Z3 调度的两个入口:直线调度(min makespan)与循环调度(min II + buffer 份数) 已核验(2026-10-01):逐行阅读;未运行
S5 同上 :17, 132, 391-420, 431-436, 442-455, 456-468, 472-499, 503-514, 518-550, 565-575, 849 全局锁、私有 z3.Context、变量、容量约束、数据/资源/owner/stage/offset 约束、二分 II、手动不可行报错、静默退化分支、seed 已核验(2026-10-01)
S6 _vendor/tilelang-v0.1.15/src/ascend/transform/ascend_pipe.h:52-62 ResourcePipe 七位掩码与 MTE1/2/3、Cube、Vector、Fixpipe、Scalar 的语义 已核验(2026-10-01)
S7 _vendor/tilelang-v0.1.15/src/ascend/transform/auto_schedule/task_annotations.h:41-42, 89-153 latency / ii 字段与 latency >= ii 校验 已核验(2026-10-01)
S8 _vendor/tilelang-v0.1.15/src/ascend/transform/auto_schedule/dependency_analysis.h:38-76 kData / kOwnerExclusion 两类依赖与 distance 三档语义 已核验(2026-10-01)
S9 _vendor/tilelang-v0.1.15/src/ascend/transform/auto_schedule/ir_structure.h:324-333, 494, 519, 533-536 TaskNode:stage / latency / II / pipe mask / 读写区域 已核验(2026-10-01)
S10 _vendor/tilelang-v0.1.15/src/ascend/transform/estimate_latency.h:28-160 延迟与 II 估算的默认硬件参数(含 cube_throughput_fp16{8192}、mte2_gm_to_l1_base_latency{190}、valu_bandwidth{256} 等) 已核验(2026-10-01):这些是模型参数,不是实测值
S11 _vendor/tilelang-v0.1.15/src/ascend/transform/estimate_latency.cc:330-345, 941-991 延迟/II 算法(访存 + 计算、按流水取 max) 已核验(2026-10-01)
S12 _vendor/tilelang-v0.1.15/src/ascend/transform/auto_schedule.cc:245-258, 452-676, 680-745, 846-857, 1110-1144, 1272-1295, 1318-1347, 1371-1400 MergeSelectedBufferVersion 与 accepted limitation、直线/循环调度入口、FFI 取函数、容量扣减、stage 推导、overall latency、ScheduleSingleKernel 的 SetMemoryLimit 与容量预检、pass 创建 已核验(2026-10-01)
S13 _vendor/tilelang-v0.1.15/src/ascend/transform/materialize_schedule_units.cc:18-22, 578 任务边界规范化与「物化平凡调度单元」 已核验(2026-10-01)
S14 _vendor/tilelang-v0.1.15/src/ascend/transform/annotate_multi_buffer_eligible.cc:19-38, 835 多缓冲资格推断(owner 推断、显式 claim 优先级) 已核验(2026-10-01)
S15 _vendor/tilelang-v0.1.15/src/ascend/transform/insert_sync.cc:751-770, 2270-2297, 2363-2422, 2568 GetHardEvent 归一化、Set/Wait 与跨核 Set/Wait 的 IR 构造、同步插入 已核验(2026-10-01)
S16 _vendor/tilelang-v0.1.15/src/ascend/transform/rewrite_flag_to_buf.cc:3-14, 201-210, 293-294 8 槽 flag 命名空间、0/1 背包(容量 8)、溢出到 32 槽 get_buf/rls_buf 池 已核验(2026-10-01):源码描述;未在硬件上验证槽位数量
S17 _vendor/tilelang-v0.1.15/src/ascend/transform/prepare_multi_buffer.cc:19-22, 1249、assign_core.cc:401、resolve_core.cc:19-22, 559、materialize_multi_buffer.cc:19-22, 561、lower_scheduled_tir.cc:744、restore_while_loops.cc:117 解落地的各步(核分配、多缓冲时钟、版本物化、body 生成) 已核验(2026-10-01)
S18 _vendor/tilelang-v0.1.15/src/ascend/transform/attr.h:18-39 tl.ascend_per_core_task / tl.ascend_task / tl.ascend_stage / tl.schedule_unit 四个属性 已核验(2026-10-01)
S19 _vendor/tilelang-v0.1.15/tilelang/ascend/language/schedule_hint.py:13, 65, 118, 196, 246 T.PerCoreTask / T.Task / T.Stage / T.assume_no_conflict / T.assume_conflict 已核验(2026-10-01)
S20 _vendor/tilelang-v0.1.15/tilelang/ascend/language/annotations.py:19, 41-46, 107, 165 T.annotate_buffer_versions(含「启发式漏读时用 {buf: 1}」)/ annotate_manual_multi_buffer / annotate_unlimit_memory 已核验(2026-10-01)
S21 _vendor/tilelang-v0.1.15/tilelang/ascend/language/sync.py(ascend_set_flag / ascend_wait_flag 的 docstring) 生成 AscendC::SetFlag<AscendC::HardEvent::XXX>(id) 的语义 已核验(2026-10-01)
S22 _vendor/tilelang-v0.1.15/tilelang/ascend/README.md:141-152 与手写 Ascend C 的分工对照表,含 Synchronization: None (Just write operations in order!) 已核验(2026-10-01)
S23 _vendor/tilelang-v0.1.15/examples/ascend/example_gemm.py:60-80, 98 T.Persistent + T.Pipelined(num_stages=NUM_STAGES) 的 GEMM;do_bench(..., backend="msprof") 已核验(2026-10-01):文件存在,API 逐个比对;未运行
S24 _vendor/tilelang-v0.1.15/tilelang/cuda/pipeline.py:129-130, 251, 258-259 CUDA 侧对应位置是 PipelinePlanning / InjectSoftwarePipeline / MergeSharedMemoryAllocations / ThreadSync 已核验(2026-10-01)
S25 _vendor/tilelang-v0.1.15/src/transform/inject_pipeline.cc:1495-1550 CUDA 多缓冲版本数 = use - def + 1(结构化上界),含 num_versions == 2 特例 已核验(2026-10-01)
S26 _vendor/tilelang-v0.1.15/src/cuda/transform/auto_schedule/role_based_scheduler.h:17-22 CUDA 的「自动调度」是 Fixed-role heuristic,不是求解器 已核验(2026-10-01)
S27 _vendor/tilelang-v0.1.15/tilelang/transform/pass_config.py:76, 338-342 TL_ENABLE_AUTO_SCHEDULE = "tl.enable_auto_schedule"、TL_ENABLE_DUMP_IR / TL_DUMP_IR_DIR 已核验(2026-10-01)
S28 _vendor/tilelang-ascend-v0.1.2.000/docs/TileLang-Ascend Programming Guide.md:194-232, 1094-1205, 2527-2560 A5 Camodel 仿真(含指令/UB dump)、T.Pipelined 的 intra-core / inter-core 用法、msProf 调优 已核验(2026-10-01):本地文档快照;未运行

corpus 资料引用

id 引用位置 用途 核验状态
C1 DeepSeek 开源昇腾基础组件将会带来哪些影响? 第 1 节引用的「脑内时序图」原话、模型五要素的自述、latency vs issue interval、「Z3 不懂 Cube」 community 级(来源 id zhihu-dazhiruoyu-deepseek-ascend,trust: community);本地全文已核验(2026-10-01),性能类说法一律视为未确认
C2 昇腾路线:现状、差异与入手点(编者笔记) 昇腾路线综述(任务化/资源建模/Z3 求解/落地四段)、机器模型对照表 corpus 内合成文档,已核验(2026-10-01);其版本提示要求以 _vendor/ 为准
C3 TileLang 编译期内部地图(编者笔记) 第 3.2 节 PipelinePlanning + InjectSoftwarePipeline 的作用与位置 已核验(2026-10-01);同样以 _vendor/ 为准

上游链接

id URL 用途 核验状态
U1 https://pypi.org/project/z3-solver/ 本章玩具模型可选依赖 z3-solver 的安装来源 已核验可达(2026-10-01,HTTP 200)
U2 https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/850/devaids/optool/atlasopdev_16_0082.html msProf 算子调优官方文档(第 5.5 节实验引用) 已核验可达(2026-10-01,HTTP 200)
U3 https://github.com/Z3Prover/z3 Z3 上游仓库 未核验:本环境访问 GitHub 超时(curl 返回 000)
U4 https://github.com/tile-ai/tilelang TileLang 上游仓库 未核验:同上
U5 https://www.zhihu.com/question/2088578683623814216/answer/2088624284100999121 C1 的原始出处 未核验:curl 返回 HTTP 403(反爬);本章以 corpus 内全文快照为准

环境核验记录(本章实际执行的命令)

id 命令 结果 含义
V1 ls _vendor/tilelang-v0.1.15/tilelang/ascend/transform/ 有 __init__.py 与 z3_scheduler.py 求解器在主干仓库(不是适配器仓库)
V2 ls _vendor/tilelang-v0.1.15/src/ascend/transform/ 34 个 .cc + auto_schedule/ 子目录 13 个 pass 的实现都在主干
V3 python3 code/pass_order/ascend_pass_order.py 13 个 pass,顺序一致,全部找到 FFI 注册点,退出码 0 第 4.1 节表格的来源
V4 python3 code/toy_scheduler/toy_schedule.py demo II = 28、buffer versions = [2] 第 4.3 节的玩具实例结果
V5 python3 code/toy_scheduler/toy_schedule.py sensitivity 低估 MTE2 延迟(40→16)后调度器给出 II = 16,在真值模型下报出 24 cycle 违约 第 5.1 节的证据(玩具规模)
V6 把玩具实例 memory_groups 改成 [[1500, 0]] 后求解 II = 55、buffer versions = [1] 习题 E10 的答案
V7 grep -rl "AutoSchedule" _vendor/tilelang-ascend-v0.1.2.000/src _vendor/tilelang-ascend-v0.1.2.000/tilelang 无输出 适配器仓库 v0.1.2.000 里没有 AutoSchedule
V8 python3 -c "import z3" ModuleNotFoundError: No module named 'z3' 本机没有 z3-solver;--z3 分支未验证
V9 grep -rn "ascend_auto_cv_combine" _vendor/tilelang-v0.1.15/ 无输出;该键只出现在适配器仓库文档与 skill 说明里 第 5.4 节标「待核」的依据

待核事项(需要后续确认,本章不自行补全)

  1. tl.ascend_auto_cv_combine / tl.ascend_auto_cross_core_sync 的状态。 适配器文档 要求使用核间流水线时必须开启,但 v0.1.15 的 pass_config.py 里没有这两个键。需要确认: 它们是被主干的新机制取代了,还是尚未合入。
  2. RewriteFlagToBuf 的 8 槽 / 32 槽是否对应真实硬件限制。 源码如此描述,但没有 厂商文档佐证,也未在硬件上验证。
  3. EstimateLatency 默认参数的目标芯片。 estimate_latency.h 的注释只说明 「convention」与单位,没有写这些数字对应哪一代昇腾(cube_throughput_fp16{8192} 是否只对 950 成立)。需要确认标定来源与适用范围。
  4. 知乎原文的性能对比结论(「与主线 Ascend C 打得有来有回」)属 community 级, 需要独立复测;本章只作为社区说法引用,未当作结论。

待补资料(建议后续版本处理)

  • corpus 内没有 Z3 自身的资料(论文或手册)。本章只在概念层面提到 SMT, 未引用 Z3 的算法细节;如果后续要讲「为什么 Z3 比启发式更合适」,需要补一条 Z3 的一手来源。
  • corpus 内没有昇腾官方关于 SetFlag/WaitFlag 槽位数量的硬件手册条目。

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