跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 01 章背景、设计思想与架构25 分钟14 习题7 自测题

为什么需要 Tile DSL

从「为什么 PyTorch 里一个算子会慢」讲起:存储层级的不对称、算术强度与机器平衡点,然后沿着 SIMT → 张量核 → 异步搬运三代硬件演进,说明为什么 CUDA、Triton、TileLang 会依次出现。

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

学习目标

读完本章,你应当能够:

  1. 用「算术强度(arithmetic intensity)」与「机器平衡点(machine balance)」解释:为什么同一个 softmax 或 MatMul,在 PyTorch 里调用时可能只跑出硬件峰值的一小部分。
  2. 说清存储层级(HBM/DRAM → 片上 SRAM → 寄存器)与「搬数据比算数据贵」之间的关系,并给出 它的量化形式。
  3. 按三个阶段(SIMT 线程模型 → 张量核 → 异步搬运与专用存储)说明硬件演进分别在编程模型上 增加了什么约束,以及这些约束为什么让「手写 CUDA 就够了」这个前提不再成立。
  4. 对照说明 CUDA、Triton、TileLang 三代方案各自解决了什么、又留下了什么问题。
  5. 给出「tile(分块)」在本书中的精确定义(三个要素),并指出它在 TileLang 的语言层与编译期 分别落在哪里。
  6. 画出 Halide → TVM → Triton → TileLang → cuTile/PTO-DSL 的谱系,并说清每一代的关键取舍。

前置知识

  • 会用 Python、写过 PyTorch(能读懂 x + y、A @ B、softmax(x, dim=-1))。
  • 知道矩阵乘法的定义,知道张量就是多维数组。
  • 不需要任何 CUDA、编译器或芯片知识。本章出现的每个硬件名词都会先用一两句话说明它解决 什么问题,再给术语。现在看不懂的细节可以先跳过:第 2 章展开性能模型,第 5 章做横向对比, 第 6 章给两台机器的抽象图。

本章是全书的开篇,所以它只回答一个问题:为什么「在 PyTorch 里写模型、把慢的部分丢给 手写 CUDA」这条路,今天已经走不通了,以及为了解决它,编程模型是怎样一步一步变成现在 这个样子的。


1. 问题的由来:为什么 PyTorch 里一个算子会慢

1.1 一次「慢」,慢在哪里

先看一段任何 PyTorch 用户都写过的代码:

import torch

x = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)
w = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)

y = torch.softmax(x, dim=-1)   # 逐元素 + 沿行归约
z = y @ w                      # 矩阵乘

如果拿性能分析器去看,通常会发现两件事:

  • z = y @ w 往往离厂商库(cuBLAS)的峰值不远,因为 PyTorch 会调用一个高度优化的 矩阵乘 kernel;
  • 但在真实模型里,大量时间花在许多个小算子上——归一化、激活、逐元素缩放、类型转换、 attention 里的 softmax——它们单个都不慢,加起来很慢。

「慢」不是一个笼统的感觉,它有具体的来源。请把一次算子执行拆成两类动作:

  1. 搬运:把数据从显存搬到计算单元旁边,或者从计算单元搬回去。
  2. 计算:对这些数据做加、乘、比较、归约。

直觉上我们会以为时间花在「计算」上,但在真实的 AI 算子里,时间经常花在搬数据和等数据 上。NVIDIA 官方文档里的两句话说明了这种不对称:

「When accessing uncached local or global memory, there are hundreds of clock cycles of memory latency.」 「The latency of most arithmetic instructions is typically 4 cycles on devices of compute capability 7.0.」 —— 来源:CUDA C++ Best Practices Guide (CUDA C++ Best Practices Guide,抓取日期 2026-09-30)。测量条件:CUDA 编程模型下, 未缓存的 local/global 访存延迟;算术延迟以 compute capability 7.0 设备为例。

也就是说:同一次访存要等上百个时钟周期,而同一次浮点运算通常只要几个时钟周期。 这件事决定了几乎所有算子优化的方向——不是「算得更快」,而是「少搬一点、把计算搬到数据 到达的位置」。

1.2 三个具体的「慢」

把上面的直觉落到代码上,一个算子慢通常有三种原因,它们常常同时存在:

(a)访存受限(memory-bound)。 算子的计算量太小,带宽成了瓶颈。典型例子是逐元素 运算、RMSNorm、softmax。以 softmax(x, dim=-1) 为例,无论怎样优化,你至少要把 x 从 显存读一遍、把结果写一遍;如果实现不当(比如先减最大值走一遍、再 exp 走一遍、再求和走 一遍),访存次数会成倍增加。

(b)没有把数据留在片上。 如果每一轮计算都要重新从显存取一遍同样的数据,那么即使算法 本身是对的,实际访存量也会远大于理论最小值。矩阵乘就是最好的例子:朴素的 A @ B 会对 同一块 A、B 反复访存。

(c)算力没有被喂饱。 现代加速器里最贵的计算单元(张量核 / Cube)是被专门设计来 做矩阵乘的,它的峰值极高但不「自动」:你必须把数据整理成它要的形状,并且让它几乎不停。 只要数据没跟上,或者每次只喂一点点,峰值就用不上。

1.3 用 roofline 量化「慢」

上面的三种原因可以统一到一个简洁的模型里。定义算术强度:

算术强度 AI = 浮点运算次数 / 访问的字节数        (单位:FLOP/Byte)

再定义机器的平衡点:

机器平衡点 = 峰值算力 / 峰值带宽                 (单位:FLOP/Byte)

于是:

  • 如果 AI < 机器平衡点,机器会先被带宽卡住,称为 memory-bound;
  • 如果 AI > 机器平衡点,机器会先被算力卡住,称为 compute-bound。

举一个可以自己核对的例子。据 NVIDIA 公开资料,H100 SXM5 使用 HBM3,「delivering over 3 TB/sec of memory bandwidth」,同时它的 FP16 张量核密集吞吐在厂商公开的初步规格表 里是 1000 TFLOPS:

来源:NVIDIA Hopper Architecture In-Depth (NVIDIA Hopper Architecture In-Depth,2022)。条件:H100 SXM5,厂商公开资料与 「Table 1. NVIDIA H100 Tensor Core GPU preliminary performance specs」,属未实测的 厂商初步规格。

用这两个数粗略估算(本书的推导,不是厂商给的数字):

机器平衡点 ≈ 1000e12 / 3.0e12 ≈ 333 FLOP/Byte

也就是说,H100 要跑到峰值,平均每访问 1 字节就得做 300 多次浮点运算。现在算一下两个 算子的算术强度(假设 fp16、按理论最小访存量计算):

算子 运算量 最小访存量(fp16) 算术强度
softmax, 按每元素 3 FLOP 粗估 ≈ 50 MFLOP 读 32 MiB + 写 32 MiB 0.75 FLOP/Byte
MatMul, FLOP 字节 = 96 MiB ≈ 1365 FLOP/Byte

(上表由 code/01-op-cost-model/op_cost_model.py 复算,数值即该脚本的实际输出。)

结论很清晰:softmax 这类算子的算术强度比平衡点低两个数量级,它天生就是 memory-bound;而大矩阵乘天生是 compute-bound。 两者的优化方向完全不同:

  • memory-bound 的算子要减少访存次数(融合、复用片上数据);
  • compute-bound 的算子要把数据留在片上、喂满张量核。

本书 code/01-op-cost-model/ 里的脚本就是上面这张表的可运行版本,你可以改形状和机器参数 自己复算。

1.4 小结:慢的根源是「数据没有待在它该待的地方」

到这里可以给本章的核心判断下一个断言:

AI 算子的性能问题,本质上是数据在存储层级之间流动的问题;优化一个算子,主要不是 在改数学公式,而是在改「数据什么时候、以什么形状、在哪一级存储上被读取和计算」。

这就解释了为什么我们需要一门面向数据流、以「片」为单位的语言。下一节先把这套 「存储层级 + 分片」的词汇定义清楚。


2. 概念与模型

存储层级对照:NVIDIA GPU vs 昇腾 NPUNVIDIA GPU(以 Hopper 为例)华为昇腾 950(对照)最快、容量最小寄存器 / Register FileL0A / L0B / L0C(Cube 专用存储)片上 SRAMShared Memory(与 L1 合并共 256 KB,shared 可配到 228 KB)L1(CBuf)片上缓存 / Vector 侧L2 Cache(H100:50 MB)UB / Unified Buffer(Vector 侧片上存储)最慢、容量最大HBM / 显存(H100 SXM5:HBM3,over 3 TB/sec)GM / 全局内存
两台机器的存储层级:越靠近计算单元,容量越小、延迟越低、带宽越高。
  • 越往下:越大、越慢、越便宜。
  • 两边不是一一对应——L0A/L0B/L0C 在 NV 侧没有同名对应物;NV 的 shared memory 在昇腾侧要拆成 L1 与 UB 两种角色。
  • 完整抽象对照见第 6 章;NV 侧的容量数字为 H100 厂商公开资料。
元素 / 线程视角 vs tile 视角元素 / 线程视角(CUDA)tile 视角(TileLang)第一步for each element:决定属于哪个线程声明 tile 的形状与存储位置第二步决定访存模式,决定 bank conflict用 T.copy 描述搬运第三步手写同步点用 T.gemm 描述计算第四步手写数据搬运编译器推导 layout,规划流水与同步
同一件事在两种视角下的描述方式:线程视角全部由人决定,tile 视角把布局、流水与同步留给编译器。
  • 左列是第 4.1 节要讲的 CUDA 路线,右列是第 4.4 节要讲的 TileLang 路线。
  • tile 视角里「我」要描述的是:这块 block_M × block_K 的 A 放哪、和哪块 B 相乘、结果累积到哪(见正文 2.4 节)。

2.1 存储层级:谁是「近」的,谁是「远」的

所有现代加速器都遵循同一张图:越靠近计算单元,容量越小、延迟越低、带宽越高。

存储层级链条:NVIDIA GPU vs 昇腾 NPUNVIDIA GPU(以 Hopper 为例)华为昇腾 950(对照)寄存器 / Register File最快,容量最小Shared Memory / 片上 SRAM快,容量有限L2 Cache中等HBM / 显存最慢,容量最大L0A / L0B / L0CCube 专用存储L1(CBuf)UB / Unified BufferVector 侧片上存储GM / 全局内存
两台机器各自的存储链条:越靠近计算单元,容量越小、延迟越低、带宽越高。
  • 两台机器的层级不是一一对应的:L0A / L0B / L0C 是 Cube 专用存储,NVIDIA 侧没有同名对应物。
  • NVIDIA 的 shared memory 在昇腾侧要拆成 L1 与 UB 两种角色;完整展开见第 6 章。

图 1:两台机器的存储层级对照。NV 侧的层级来自 CUDA C++ Programming Guide;昇腾侧的层级来自 DeepSeek 开源昇腾基础组件将会带来哪些影响? (社区一手文章,非厂商官方文档)与 _vendor/tilelang-v0.1.15/tilelang/ascend/README.md:134-139。完整展开见第 6 章。

两个细节值得现在就记住:

  • 两台机器的层级不是一一对应的。 昇腾的 L0A/L0B/L0C 是 Cube(矩阵单元)的专用存储, 在 NVIDIA 侧没有一个同名的对应物;反过来,NVIDIA 的「shared memory」在昇腾上要拆成 L1 与 UB 两种角色。
  • 数据「在哪里」比数据「是什么」更重要。 TileLang 里每个 buffer 都要声明它的存储作用域 (storage scope),因为放在哪一级直接决定了后续能不能被某类计算单元直接消费。

2.2 「搬」和「算」可以重叠

如果只有上一节的内容,结论会是「尽量减少搬运」。但现代硬件还有一个更强的能力: 让搬运和计算同时进行。

做法是把一次搬运所在的缓冲区开成多份(多缓冲 / multi-buffer):第 0 份数据在被计算时, 第 1 份正在被搬运。理想情况下,搬运的延迟被完全藏在计算的背后,机器不再「等数据」。

这带来两个必须管理的资源约束:

  • 片上容量:多开一份就多占一份片上空间,而片上空间是有限的。据 NVIDIA 公开资料, H100 每个 SM 有 256 KB 的 shared memory + L1 合并空间,其中 shared memory 可配置 最多 228 KB(来源:NVIDIA Hopper Architecture In-Depth, H100 厂商公开资料)。份数不可能无限加。
  • 同步:当搬运是异步的(「已经发出去了」不等于「已经做完了」),就必须用某种信号 机制保证「计算开始前数据真的到了」「下一轮覆盖前上一轮真的读完了」。在 CUDA 里这套 机制是 cp.async 的 commit/wait、mbarrier;在昇腾里是 MTE 流水之间成对的 Set/Wait。

2.3 什么是 tile

现在可以给出本书里最重要的一个定义。tile(分块) 是:

对一个逻辑张量的一次「定形的取样」,它同时携带三件事:

  1. 形状(shape)——这块数据在逻辑张量里的定形子区域(静态形状);
  2. 存储位置(storage scope)——这块数据被放在哪一级存储上;
  3. 物理布局(layout)——逻辑下标到物理(线程、寄存器槽位、存储地址)的映射。

这三条缺一不可,尤其是第 3 条经常被忽略:

  • 少了第 2 条,tile 就只是「循环分块(loop tiling)」,一种循环变换技巧;
  • 少了第 3 条,tile 就只是「一块数据」,编译器还得替你在物理上重新排列一遍。

这不是本书自创的说法。Triton 2019 年的原始论文把 tile 定义为 「statically shaped multi-dimensional sub-arrays」(定形的多维子数组):

来源:Triton: An Intermediate Language and Compiler for T… (Triton, MAPL 2019,DOI 10.1145/3315508.3329973),第 1 页摘要。

TileLang 官方文档给出的说法更贴近工程:

「A tile represents a shaped portion of data, which can be owned and manipulated by a warp, thread block, or equivalent parallel unit.」 来源:TileLang 官方文档 · 资料清单 (Tile Language: A Brief Introduction)。

把两者合起来,就是本书的用法:tile 是「程序里最小的可寻址数据单元」,也是一个可以被 某个并行单元(warp / thread block / Cube 核)拥有和操作的定形数据片。

2.4 以 tile 为单位思考,和以线程为单位思考有什么不同

对比一下同一件事在两种视角下的描述方式。以 C = A @ B(, )为例:

视角 第一等公民 「我」要描述什么 谁负责决定
CUDA(SIMT) 线程(thread) 第 (i, j) 个线程算哪几个元素 全部由人决定
tile 视角 tile 这块 block_M × block_K 的 A 放哪、和哪块 B 相乘、结果累积到哪 布局/流水/同步可由编译器决定

线程视角的问题是:它是「机器视角」。程序员被迫在写算法的时候同时思考线程编号、 bank conflict、同步点。而 tile 视角允许你先说「我要把 A 的一块搬到片上,和 B 的一块做 矩阵乘」,把「谁来做、怎么排布、什么时候同步」留给编译器——这就是本书后续要展开的核心 主张。

两条流水:元素 / 线程视角 vs tile 视角元素 / 线程视角(CUDA)tile 视角(TileLang)for each element决定属于哪个线程决定访存模式决定 bank conflict手写同步点手写数据搬运声明 tile 的形状与存储位置用 T.copy 描述搬运用 T.gemm 描述计算编译器推导 layout规划流水与同步
同一件事在两种视角下的写法:线程视角每一步都由人决定,tile 视角把布局、流水与同步交给编译器。
  • 左列是第 4.1 节的 CUDA 路线,右列是第 4.4 节的 TileLang 路线。
  • tile 视角里「我」要说的是:这块 A 放哪、和哪块 B 相乘、结果累积到哪(见正文 2.4 节)。

图 2:两种编程视角的对照。左列是第 4.1 节要讲的 CUDA 路线,右列是第 4.4 节要讲的 TileLang 路线。


3. 用法:一个 tile 程序长什么样

本节只展示「长什么样」,不解释语法细节——语言基础在第 8 章,原语在第 9 章。

3.1 从元素视角的伪代码

如果用最朴素的「元素视角」描述 C = A @ B + ReLU,你会写出这样的伪代码:

for i in range(M):
    for j in range(N):
        acc = 0
        for k in range(K):
            acc += A[i, k] * B[k, j]
        C[i, j] = max(acc, 0)

这段代码在数学上完全正确,但直接映射到 GPU 上会反复访存 A 和 B,且完全没用上 张量核。注意:这段伪代码里没有任何地方提到数据放在哪一级存储、以什么布局排布。

3.2 tile 视角:分块 → 上片 → 计算 → 回写

TileLang 给出的写法则显式地把上面缺的信息补上。下面这段与锁定的 v0.1.15 官方 quickstart (_vendor/tilelang-v0.1.15/examples/quickstart.py)一致,只去掉了 import 与一处被注释 掉的 T.use_swizzle;它同时包含矩阵乘和一个 ReLU 后处理:

@tilelang.jit
def matmul(A, B, block_M: int, block_N: int, block_K: int):
    M, N, K = T.const("M, N, K")
    dtype = T.float16
    accum_dtype = T.float32
    A: T.Tensor((M, K), dtype)
    B: T.Tensor((K, N), dtype)
    C = T.empty((M, N), dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
        A_shared = T.alloc_shared((block_M, block_K), dtype)         # 片上
        B_shared = T.alloc_shared((block_K, block_N), dtype)         # 片上
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)  # 累加器

        T.clear(C_local)
        for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=3):
            T.copy(A[by * block_M, ko * block_K], A_shared)          # 搬运
            T.copy(B[ko * block_K, bx * block_N], B_shared)          # 搬运
            T.gemm(A_shared, B_shared, C_local)                      # 计算

        for i, j in T.Parallel(block_M, block_N):
            C_local[i, j] = T.max(C_local[i, j], 0)                  # 融合的 ReLU

        T.copy(C_local, C[by * block_M, bx * block_N])               # 回写
    return C

请只注意结构与上一节伪代码的差别,不要纠结 API 细节:

  1. 网格从「元素」变成了「块」:T.Kernel(...) 的每次迭代负责输出矩阵的一个 block_M × block_N 分块。这是 tile 视角的第一层含义。
  2. 数据被显式声明了存储位置:T.alloc_shared(片上)、T.alloc_fragment(累加器)。
  3. 搬运被显式描述:T.copy 说的是「把 A 的这一块搬到片上」,不是「第几个线程搬第 几个元素」。
  4. 计算被显式描述:T.gemm 说的是「对这两个片上 tile 做矩阵乘」,而不是循环。
  5. 循环被标记为流水:T.Pipelined(..., num_stages=3) 说的是「这三份缓冲轮转起来, 让搬运和计算重叠」,而不是「我手写一个多缓冲」。

第 3、4、5 点正是「调度空间」——在 CUDA 里它们全部是人的工作,在 TileLang 里它们是可以 被编译器接管的声明。第 3 章会解释为什么 TileLang 选择把它们留在语言里,而不是像某些 高层 DSL 那样完全隐藏。

本机限制:本教程的写作环境没有 NVIDIA GPU,也没有昇腾 NPU。上面这段代码摘自锁定 版本的官方示例,只做了 API 存在性与签名核对(见本章 本章引用),没有在本机 运行过。运行方式见 资料库索引。


4. 实现机制:三代方案的问题演进

本节按时间顺序讲三次「问题 → 方案 → 遗留问题」。它同时也是本章标题的答案:为什么 在 CUDA 和 Triton 之后,还需要 TileLang 这样一门 tile DSL。

硬件三阶段与「必须人工管理的资源」的增长阶段一:SIMT 线程阶段二:张量核阶段三:异步 + 专用存储本阶段新增线程分工 + 片上搬运+ fragment 布局(矩阵指令约束)+ 多流水同步协议 + 多缓冲容量约束 + 跨核 / 跨 SM 依赖人管资源数2 类资源3 类资源5+ 类资源(互相约束 → 组合爆炸)代表硬件Kepler / PascalVolta 起Hopper / Blackwell;昇腾 950
每一阶段都在编程模型上新增一层必须显式管理的资源;到第三阶段,「手写 CUDA 就够了」这个前提不再成立。
  • NV 侧事实来源:NVIDIA Hopper 架构公开资料(TMA、thread block cluster、distributed shared memory)。
  • 昇腾侧来源:社区一手文章(非厂商官方文档)与 _vendor/tilelang-v0.1.15/tilelang/ascend/pipeline.py:108-114。
CUDA / Triton / TileLang 的信息分工演进CUDATritonTileLang第一等公民线程tiletile + 显式数据流人负责线程分工 / 搬运 / 布局 / 同步 / 流水tile 级算法与索引数据流 + 资源信息(放哪、怎么流)编译器负责指令选择 / 寄存器分配tile 内部布局 / 线程分配 / 调度布局推导 / 流水 / 同步 / 多缓冲演进全部手工调度交给编译器调度空间解耦后可表达主要遗留问题复杂度随硬件代际指数增长,正确性难验证tile 与调度绑定,硬件资源信息无法显式表达需要更多显式决策;抽象仍在快速演化
三代方案的信息分工:TileLang 让该显式表达的调度信息回到代码里,把能推导的留给编译期 pass。
  • 图中「布局推导 / 软件流水 / 同步插入由编译期 pass 完成」对应 TileLang 这一列。
  • CUDA 的事实来源见正文 4.1 节,Triton 的设计取舍见 MAPL 2019 论文,TileLang 的解耦声明见 arXiv:2504.17577。

4.1 第一代:CUDA —— 用线程模型换到通用性

CUDA 的历史地位无可替代:它第一次让「在通用硬件上写并行程序」变得可行。它的核心抽象是 SIMT 线程模型:

Grid → Thread Block → Warp → Thread

程序员写的是「第 threadIdx.x 个线程做什么」,硬件负责把同一 warp 内 32 个线程的指令 一起发射。要写一个高性能算子,你需要手工处理:

  • 把数据从 global memory 搬到 shared memory(以及要不要用 cp.async);
  • shared memory 的 bank conflict 与 swizzle(数据摆放方式,避免多个线程抢同一 个存储体);
  • 什么时刻插 __syncthreads(),什么是 mbarrier;
  • 哪些线程负责搬数据、哪些负责算;
  • 不同算力代际要用不同的指令(mma → wgmma → tcgen05)。

这套体系是能写出世界最快算子的,FlashAttention、CUTLASS 都是它的产物。但它留下 的问题是致命的:

  • 每次硬件换代,经验都要重学一遍。 张量核出现后,数据必须排成特定 fragment 布局; 异步搬运出现后,同步协议从「一个 __syncthreads」变成「多数量的 mbarrier 相位」。
  • 复杂度是组合的,不是线性叠加的。 存储位置 × 布局 × 同步 × 流水 × 算力单元, 每加一个维度,需要人手工维持的正确性条件就翻一倍。
  • 「正确」变得难以验证。 同步少放一处,往往不是编译报错,而是偶发的数值错误。

一句话:CUDA 把「通用性」给了程序员,代价是把「所有硬件细节」也一起给了程序员。

4.2 硬件是怎么把「手写 CUDA 就够了」这个前提打破的

过去十多年,加速器沿着三条线演化。每一条都在编程模型上新增一层必须被显式管理的资源。

阶段一:SIMT 线程模型(Kepler / Pascal 时代)。 程序员只需要关心两件事:线程怎么分工、数据在 global 和 shared 之间怎么搬。这一阶段 「手写 CUDA」确实是通解。CUDA 官方的编程手册到今天仍以这套模型为基础 (CUDA C++ Programming Guide)。

阶段二:张量核(Tensor Core)。 从 Volta 起,GPU 里多出一个专用的矩阵乘单元,它的吞吐远高于通用浮点单元,但要求 输入必须是特定的 fragment 布局、累加必须发生在特定的寄存器组上。于是「数据怎么摆」第一次 成了一个必须显式表达、否则直接拿不到性能的约束。程序员的工作从「排线程」变成了 「排线程 + 对布局」。

阶段三:异步搬运与专用存储(Hopper / Blackwell,以及昇腾 950)。 这一阶段的变化最彻底。以 Hopper 为例,官方架构说明里写:

「NVIDIA H100 is the first truly asynchronous GPU. H100 extends A100's global-to-shared asynchronous transfers across all address spaces...」 「New asynchronous execution features include a new Tensor Memory Accelerator (TMA) unit that can transfer large blocks of data efficiently between global memory and shared memory.」 来源:NVIDIA Hopper Architecture In-Depth (NVIDIA Hopper Architecture In-Depth,2022;条件:H100,厂商公开资料)。

再加上 thread block cluster 与 distributed shared memory,「什么时候搬、谁在等谁」这 张时间表,已经从「一个 block 内的事」升级为「多个 SM 之间、多条异步流水之间的协议」。 Blackwell 又新增了张量内存(TMEM)与 tcgen05 系列 MMA,把「累加器住在哪」也变成 必须显式管理的对象。

昇腾侧是同构的问题、不同的语言。社区一手文章对昇腾 950 的描述是:

「Cube/AIC 和 Vector/AIV 从一开始就承担着不同类型的计算,数据又要在 L1、L0A、L0B、 L0C、UB 这些具有明确角色的存储之间流动,再加上 MTE 等搬运流水,很多在 CUDA 里默认 附着在线程层次上的事情,到了这里必须重新说清楚到底由哪种资源负责。」 来源:DeepSeek 开源昇腾基础组件将会带来哪些影响? (知乎「乱序摸鱼」,2026;社区文章,非厂商官方文档,数字与结论未经官方复核)。

同一篇文章还指出,把「GPU 是 SIMT、NPU 是 SIMD」当作概括已经失效了——昇腾 950 后端 本身就同时支持 SIMT 与 SIMD 两种向量执行方式。这件事在第 6 章会展开。

阶段三的必然后果是:手写 CUDA 的成本开始指数上升。 不是因为 CUDA 变难了,而是因为 「需要被同时正确管理的东西」变多了:

三阶段硬件演进与新增的管理对象阶段一SIMT 线程管理对象线程分工 + 搬运阶段二张量核管理对象+ fragment 布局阶段三异步 + 专用存储管理对象+ 多流水同步 + 多缓冲容量+ 跨 SM / Cube-Vector 依赖
每一阶段都在编程模型上新增一层必须人工管理的资源;到第三阶段,要同时正确管理的东西开始互相约束。
  • NV 侧的三阶段事实来源见正文 4.2 节引用的 Hopper 架构资料;昇腾侧的对照见第 6 章。
  • 第三阶段是「手写 CUDA 就够了」失效的直接原因:管理对象从线性叠加变成组合爆炸。

图 3:硬件演进与「必须人工管理的资源」的增长。前三阶段的 NV 侧事实来源同 4.2 节引用的 Hopper 架构资料;昇腾侧的对照见第 6 章。

4.3 第二代:Triton —— 把 tile 变成语言的第一等公民

Triton 的关键贡献,是把上一节的复杂度从人手里拿走一部分:让程序员以 tile 为单位 写程序,让编译器负责「tile 内部怎么分配线程、怎么合并访存、怎么排布」。

Triton 2019 年的原始论文里,作者把当时的 DNN 编译器分成三类,并说明自己的路线:

「By contrast, Triton relies on the addition of tile-level operations and optimizations into traditional compilation pipelines. This approach provides (1) more flexibility than XLA and Glow; (2) support for non-affine tensor indices contrary to TC and Diesel; and (3) automatic inference of possible execution schedule that would otherwise have to be specified manually to Halide or TVM.」 「The benefits of Triton come at the cost of increased programming efforts.」 来源:Triton: An Intermediate Language and Compiler for T… (Triton, MAPL 2019),第 3 页。

它解决了什么: 用 tile 描述数据并行,程序员不必再手写「哪个线程处理哪个元素」、 不必再手工处理合并访存(coalescing)与共享内存布局;同一份代码更容易跨 GPU 代际复用。

它留下了什么: 「tile 是什么」和「这个 tile 怎么被调度」被绑在一起交给了编译器。当 硬件要求你显式说出「这块数据放 L1 还是 UB」「这个中间结果留在片上还是写回」时, 在 Triton 里你要么表达不出来,要么一「下钻」就离开它的抽象、退化回手写低层代码。论文 自己也承认这条路「comes at the cost of increased programming efforts」。

4.4 第三代:TileLang —— 显式 tile + 把「调度空间」解耦出来

TileLang 的主张写在它的论文摘要里:

「TileLang decouples scheduling space (thread binding, layout, tensorize and pipeline) from dataflow, and encapsulated them as a set of customization annotations and primitives. This approach allows users to focus on the kernel's data-flow itself, while leaving most other optimizations to compilers.」 来源:TileLang: A Composable Tiled Programming Model for … (TileLang, arXiv:2504.17577, 2025),摘要。

这句话可以拆成两半:

  • 数据流(dataflow)留给人:tile 的形状、放在哪一级存储、怎么在存储之间流动、 在哪一级做计算——这些是算法作者真正知道、也应该负责的信息。
  • 调度空间(scheduling space)交给编译器:线程绑定、物理布局、tensorize(把运算 映射到硬件矩阵指令)、软件流水——这些是从数据流可以推导出来的。

官方文档把接口分成三层,并且允许在同一个 kernel 里混用:

层次 面向谁 提供什么 v0.1.15 状态
Beginner(硬件无关) 只想写逻辑的人 不涉及存储层级的写法 官方文档明确标注「not yet fully implemented」
Developer(tile library) 懂一点存储层级的人 T.copy / T.gemm / T.Pipelined 等 tile 原语 稳定,日常 90% 的代码在这一层
Expert(thread primitives) 要抠极致性能的人 线程原语与硬件专用存储 稳定,是昇腾侧的主要入口

来源:TileLang 官方文档 · 资料清单 与 _vendor/tilelang-v0.1.15/tilelang/language/kernel.py:431-455(T.Kernel 的 docstring 原文把一次 launch 描述为「a grid of tile programs」)。「三层可以混用」这一点就是论文 标题里 composable 的含义。

它解决了什么: 在保留 Triton「以 tile 为单位」这个前提的同时,把「存储位置、布局、 流水、同步」这些影响合法性与性能的资源信息重新变成可以显式表达的东西;同时又不要求 人为每一条都手写实现——能推导的交给 pass 去推导。

它留下了什么: 代价是程序员要做的决策更多了。同样一个逐元素 kernel,在 Triton 里可能几行就写完;在 TileLang 里你需要说清楚数据放哪、怎么搬。写得太「高层」反而拿不到 性能——这是第 3 章和第 12 章会反复强调的坑。此外,这一层抽象目前还在快速演化:v0.1.15 里 beginner 层尚未完全实现,就是一个直接证据。

4.5 三代方案的对照

代际 代表 第一等公民 人负责 编译器负责 主要遗留问题
第一代 CUDA 线程 线程分工、搬运、布局、同步、流水 指令选择与寄存器分配 复杂度随硬件代际指数增长;正确性难验证
第二代 Triton tile tile 级的算法与索引 tile 内部布局、线程分配、调度 tile 与调度绑定,硬件资源信息无法显式表达
第三代 TileLang tile + 显式数据流 数据流与资源信息 布局推导、流水、同步、多缓冲 需要更多显式决策;抽象仍在演化
三代方案:信息分工的演进链CUDA线程为第一等公民,全部手工Tritontile 为第一等公民,调度交给编译器TileLangtile + 显式数据流,调度空间解耦后可表达布局推导 / 软件流水 / 同步插入由编译期 pass 完成
三代方案的信息分工:TileLang 把该显式表达的调度信息放回代码里,把能推导的留给编译期 pass。
  • CUDA 的事实来源见正文 4.1 节;Triton 的取舍见 MAPL 2019 论文;TileLang 的解耦声明见 arXiv:2504.17577。

图 4:三代方案的信息分工演进。CUDA 的三项事实来源见 4.1 节;Triton 的设计取舍来源见 4.3 节引用的 MAPL 2019 论文;TileLang 的解耦声明见 4.4 节引用的 arXiv:2504.17577。


5. 进阶与陷阱

tile DSL 谱系Halide (2012)算法与调度分离TVM / TensorIR (2018-2023)把调度变成可组合的 IR 变换Triton (2019-)tile 成为语言第一等公民TileLang (2025-)tile + 显式数据流;调度空间解耦cuTileNVIDIA 的 tile DSLPTO-DSL昇腾原生 tile DSL
tile 这个概念被一步步推出来的谱系:每一代都留下了一个待解决的问题。
  • Halide:调度仍是人写的,且面向图像 / 数组处理;TVM:需要使用者理解 IR 与调度原语。
  • Triton:tile 与调度绑定,硬件资源信息无法显式表达;TileLang:需要更多显式决策,抽象仍在快速演化。
  • cuTile 与 PTO-DSL:两条线各自演进,生态尚未统一;cuTile 在 corpus 中无一手资料(未核验)。

5.1 谱系:tile 这个概念是怎么一步步被推出来的

上面讲的「三代」是硬件驱动的。如果换成编程模型的视角,会得到一条更长的谱系——本书 把它概括为 Halide → TVM → Triton → TileLang → cuTile/PTO-DSL。

tile 概念的谱系:从 Halide 到 PTO-DSLHalide (2012)算法与调度分离TVM / TensorIR (2018-2023)把调度变成可组合的 IR 变换Triton (2019-)tile 成为语言第一等公民TileLang (2025-)tile + 显式数据流;调度空间解耦cuTileNVIDIA 的 tile DSLPTO-DSL昇腾原生 tile DSL
tile 这个概念被一步步推出来的谱系:每一代都留下了一个待解决的问题。
  • Halide 把「算什么」与「怎么排」分开写;TVM / TensorIR 把调度变成可组合、可搜索的 IR 变换。
  • Triton 让 tile 成为语言原语,代价是调度也一并交出;TileLang 把调度空间重新变成可选的、可注解的。
  • cuTile 与 PTO-DSL 把 tile 抽象带到各家硬件,两条线各自演进,生态尚未统一。

图 5:tile DSL 的谱系。各节点来源见下文的逐条说明。

Halide(算法与调度分离)。 Halide 提出把「算什么」和「怎么排」分开写。官方文档里 的示例把这两件事写在同一段代码的两个部分:

「// The algorithm - no storage or order ... // The schedule - defines order, locality; implies storage」 来源:Halide documentation — a language for fast, portabl…(halide-lang.org, 抓取日期 2026-09-30)。

这是整个谱系的思想起点:同一个算法可以有很多种合法且性能差异巨大的排布方式,而排布 方式应该是一种可以独立表达、独立优化的东西。

TVM / TensorIR(把调度变成 IR 变换)。 TVM 把 Halide 的思路搬到了深度学习编译器里, 并把调度做成一组可以组合、可以被搜索的 IR 变换(split/fuse/reorder/tile/vectorize)。 随后 TensorIR 把这套抽象正式化为一套 IR,使「自动调度」成为可能:

来源:TVM: An Automated End-to-End Optimizing Compiler fo…(TVM, OSDI 2018)、 TensorIR: An Abstraction for Automatic Tensorized P…(TensorIR, ASPLOS 2023)。

TileLang 的编译器骨架就建立在这条线上:它的 IR 是 TVM 的 TIR,所以「给 TileLang 加功能」 很多时候等于「写一个 TIR pass」。

Triton(tile 成为语言第一等公民)。 见 4.3 节。Triton 把 tile 从「一种循环变换」 升级成「一种语言原语」,代价是调度也一并交了出去。

TileLang(tile + 显式数据流)。 见 4.4 节。它把 Triton 交出去的调度空间重新变成 可选的、可注解的,让人可以只在自己关心的维度上干预编译器。

cuTile 与 PTO-DSL(把 tile 抽象带到各家硬件)。 这条谱系里离现在最近的一环,是两家硬件厂各自 推出面向自家芯片的 tile 级 Python DSL。昇腾官方的 PTO-DSL 项目 README 把自己的定位 写得很直接:

「PTO-DSL provides a programming abstraction similar to cuTile, but native to NPU.」 「PTO-DSL exposes more NPU-native memory hierarchy such as L0/L1/UB.」 来源:huawei-csl/pto-dsl (huawei-csl/pto-dsl,GitHub,抓取日期 2026-09-30)。

PTO 本身是一套形式化的 tile 级指令集(pto-spec/README.md 里写明它定义了「Direct Tile operations」118 条、64 个 T/U/M/N Tile 寄存器与独立的 execution mask)。这条线的意义是:tile 正在成为跨厂商的公共抽象层——不同厂商用 不同的实现去接同一个概念。第 5 章会给出这一族方案的逐项对比。

5.2 常见陷阱

  1. 把「慢」都归因于算法。 大多数 AI 算子的算法早已固定,性能差异来自数据流动方式。
  2. 把 tile 等同于「循环分块」。 少了「存储位置」和「物理布局」两要素,分块只是循环 变换,不是本书说的 tile。
  3. 把 Triton 与 TileLang 当竞品。 它们是同一问题上的两种设计取舍:Triton 换通用性 与简洁,TileLang 换可控性与跨硬件表达能力。第 5 章会做同题对照。
  4. 拿厂商宣传性能当基线。 一定要核对测量条件:dtype、shape、是否包含 epilogue、 是否含 sparse、是 dense 还是 sparse 口径。本章所有数字都尽量标了来源与条件。
  5. 把 GPU 的直觉直接套到 NPU。 存储层级、执行单元、同步协议都不同;「GPU 是 SIMT、 NPU 是 SIMD」这类概括在昇腾 950 上已经不成立(见 4.2 节引用的社区文章)。
  6. 指望 beginner 层。 v0.1.15 的官方文档明确写了这一层尚未完全实现,不要依赖它。

5.3 本章不做什么

  • 不教 TileLang 语法:第 7–9 章从安装和第一个 kernel 开始。
  • 不做横向详评:第 5 章才逐项对比 CUDA / Triton / CUTLASS-CuTe / TVM / cuTile / Ascend C / PTO-DSL。
  • 不解释两台机器的硬件细节:第 6 章。
  • 不解释编译器内部:第 4 章给全局流程,第 20–25 章深入 pass 与后端。

6. 本章小结

  • 性能困境的本质是数据在存储层级之间的流动。一次算子慢,通常慢在访存次数过多、数据 没有被留在片上、或算力单元没有被喂饱。
  • 量化工具是算术强度与机器平衡点。以 H100 的公开规格粗估,平衡点在数百 FLOP/Byte 量级;softmax 这类算子低两个数量级(memory-bound),大矩阵乘高一个数量级 (compute-bound)。两者优化方向完全不同。
  • 硬件演进沿三个阶段增加了程序员的负担:SIMT 线程模型 → 张量核(新增布局约束)→ 异步搬运与专用存储(新增多流水同步、多缓冲容量、跨核依赖)。第三阶段让「手写 CUDA 就能写好算子」这个前提失效。
  • 三代方案:CUDA 给通用性、把硬件细节全交给人;Triton 把 tile 变成语言原语、把调度 交给编译器;TileLang 把「调度空间」从「数据流」里解耦出来,让该表达的资源信息可以 显式表达,同时把能推导的部分留给 pass。
  • tile 的定义(本书口径):一个逻辑张量上的定形子区域,同时携带形状、归属、存储位置 三要素(「归属」在编译期落实为具体的物理布局 layout:谁拥有这块数据的哪个元素)。 它是 TileLang 语言层与编译期共同的核心概念。完整定义见第 3 章 §2.4。
  • 谱系:Halide(算法/调度分离)→ TVM/TensorIR(调度成为可组合 IR 变换)→ Triton(tile 成为语言原语)→ TileLang(tile + 显式数据流)→ cuTile / PTO-DSL (tile 抽象跨厂商化)。

下一章(第 2 章)会把本章的直觉补齐成一套可计算的方法:一次训练/推理迭代里算子扮演什么 角色、roofline 怎么算、torch.compile 的边界在哪里、以及什么时候必须自己写 kernel。

延伸阅读

  • 想先建立全局心智模型:第 2、3 章。
  • 想看七种同类方案的逐项对比:第 5 章。
  • 想看两台机器的抽象对照:第 6 章。
  • 想读本章引用的全部原始资料:见 本章引用,其中 R1–R12 逐条给出了 corpus 路径、 上游链接、版本与核验状态。
  • 想亲手算一遍算术强度:code/01-op-cost-model/(纯 Python,可在任何机器上离线运行)。

习题

14 题 · 答案在提交后展开

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

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

阅读 1.1 和 1.2 节,回答:

  1. 为什么说「一次算子慢,通常不是算法算错了」?
  2. 一个算子的时间可能花在哪些动作上?请举出至少两类,并各给一个本章提到的例子。
  3. 为什么同一个 softmax 在「先减最大值 → exp → 求和 → 除」的四遍实现下,会比理论上 只需要两遍的实现慢?请从「访存次数」的角度解释。

E2概念难度 ★无需硬件概念题 不需要硬件
  1. 写出算术强度(arithmetic intensity)与机器平衡点(machine balance)的定义式。
  2. 用本章的 H100 数字(峰值 FP16 张量核 1000 TFLOPS,HBM3 带宽 over 3 TB/sec)估算该 机器的平衡点,写出计算过程。
  3. 判断下面两个算子在 H100 上分别属于 memory-bound 还是 compute-bound,并说明依据:
    • 对 (fp16)做逐元素 y = max(x, 0);
    • ,其中 (fp16,fp32 累加)。

E3概念难度 ★★无需硬件概念题 不需要硬件

本章 4.2 节提出:硬件演进分三个阶段(SIMT 线程模型 → 张量核 → 异步搬运与专用存储)。

  1. 每个阶段分别给程序员新增了哪一种「必须人工管理的资源」?
  2. 为什么说「复杂度是组合的,不是线性叠加的」?请用本章的「存储位置 × 布局 × 同步 × 流水 × 算力单元」这条线索说明。
  3. 昇腾 950 的例子(Cube/Vector + L1/L0A/L0B/L0C/UB + MTE 流水)说明「GPU 是 SIMT、 NPU 是 SIMD」这个概括为什么已经失效?

E4概念难度 ★★无需硬件概念题 不需要硬件

对照 4.5 节的表格,回答:

  1. CUDA 主要把哪一类信息交给了程序员?它的通用性代价是什么?
  2. Triton 用「tile 是语言第一等公民」换来了什么?又付出了什么代价?请引用 Triton 2019 年原论文里的一句话作为依据。
  3. TileLang 相比 Triton 多保留了哪一类信息?它为什么要保留这一类信息?

E5概念难度 ★无需硬件概念题 不需要硬件

给出本书对 tile 的定义,并逐条说明缺少其中任意一条会带来什么问题:

  1. 形状(静态形状);
  2. 存储位置(storage scope);
  3. 归属(ownership)——它在编译期落实为具体的物理布局(layout)。

然后回答:为什么「对循环做分块(loop tiling)」不等价于「使用 tile 抽象」?


E6代码阅读难度 ★★无需硬件代码阅读题 不需要硬件

读下面这段来自锁定版本官方 quickstart 的代码(_vendor/tilelang-v0.1.15/examples/quickstart.py, 已省略 import):

with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
    A_shared = T.alloc_shared((block_M, block_K), dtype)
    B_shared = T.alloc_shared((block_K, block_N), dtype)
    C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
    T.clear(C_local)
    for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=3):
        T.copy(A[by * block_M, ko * block_K], A_shared)
        T.copy(B[ko * block_K, bx * block_N], B_shared)
        T.gemm(A_shared, B_shared, C_local)
    T.copy(C_local, C[by * block_M, bx * block_N])

请回答:

  1. T.Kernel 的每次迭代负责输出矩阵的哪一部分?bx、by 分别对应哪一维?
  2. 三个缓冲区分别被放在哪一级存储上?请指出你判断的依据(哪一行代码)。
  3. 这段代码里,哪些语句在描述数据流,哪些在描述调度决策?请分成两组。
  4. num_stages=3 想让哪两件事重叠?它的物理代价是什么?
  5. 如果你把 T.alloc_shared 换成 T.alloc_fragment,会有什么后果?(提示:可结合 第 2 章的存储层级,但仅凭本章也能判断。)

E7代码阅读难度 ★★无需硬件代码阅读题 不需要硬件

读下面这段昇腾 GEMM + ReLU 的例子(_vendor/tilelang-v0.1.15/tilelang/ascend/README.md:83-98):

with T.Kernel(num_blocks) as bx:
    A_l1 = T.alloc_l1((block_M, block_K), T.bfloat16)
    B_l1 = T.alloc_l1((block_N, block_K), T.bfloat16)
    C_l0c = T.alloc_l0c((block_M, block_N), T.float32)
    C_ub = T.alloc_shared((block_M // 2, block_N), T.float32)
    for tile in T.Persistent([M // block_M * n_tiles], num_blocks, bx):
        m, n = tile // n_tiles * block_M, tile % n_tiles * block_N
        for k in T.Pipelined(K // block_K, num_stages=2):
            T.copy(A[m, k * block_K], A_l1)
            T.copy(B[n, k * block_K], B_l1, l2_cache_ctrl="NOTALLOC_KEEP")
            T.gemm(A_l1, B_l1, C_l0c, transpose_B=True, clear_accum=(k == 0))
        T.dual_copy(C_l0c, C_ub)
        with T.SimtVF(threads=128):
            for i, j in T.Parallel(block_M // 2, block_N):
                C_ub[i, j] = T.max(C_ub[i, j], 0)

请回答:

  1. 这段代码里出现了哪几种「存储作用域」?请把它们按昇腾的层级从近到远排序。
  2. 为什么 C_l0c 与 C_ub 之间要用 T.dual_copy,而 A、B 用 T.copy?(提示: 阅读 _vendor/tilelang-v0.1.15/tilelang/ascend/language/copy_op.py:53-100 的 docstring。)
  3. with T.SimtVF(threads=128): 与 T.Parallel 一起出现,这里的 T.Parallel 是 在描述什么?它和 CUDA 的 threadIdx 是同一回事吗?
  4. 整段代码里没有出现任何显式的同步语句。请对照 _vendor/tilelang-v0.1.15/tilelang/ascend/README.md:144-152 的表格,说明这些同步 被谁接走了。
  5. T.gemm(...) 的 clear_accum=(k == 0) 是什么意思?如果去掉它会怎样?

E8代码阅读难度 ★★无需硬件代码阅读题 不需要硬件

读 _vendor/tilelang-v0.1.15/tilelang/language/kernel.py:431-455 中 T.Kernel 的 docstring, 回答:

  1. docstring 把一次 T.Kernel launch 描述成什么?请引用原文。
  2. docstring 说 T.Kernel 是「target-neutral launch: the part every backend shares」。 结合 tilelang/ascend/language/frame.py:107 的 T.SimtVF,说说这句话意味着什么: 同一个 tile 前端如何落到两种完全不同的执行模型上?
  3. 为什么把「网格」这个概念与「tile 程序」绑在一起(grid of tile programs),是一种 与 CUDA 的「grid of threads」不同的抽象?

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

本章 code/01-op-cost-model/op_cost_model.py 给出了一个算术强度估算脚本。请:

  1. 运行它,记录默认参数下 MatMul(4096³, fp16) 与 Softmax(4096×4096, fp16) 的算术 强度与结论。
  2. 把矩阵乘的规模改成 M=N=K=512,重算并解释结论是否改变。
  3. 把机器参数改成「峰值算力 100 TFLOPS,带宽 1.5 TB/s」,重新判断上面两个算子的类别, 并说明在什么条件下 MatMul(4096³) 会从 compute-bound 变成接近平衡点。
  4. 在脚本里再加一个 RMSNorm(按行归一化)的估算,并说明它的算术强度为什么更低。

交付:修改后的脚本 + 一小段结论(不超过 200 字)。


E10动手难度 ★★★无需硬件动手题 不需要硬件

下面是 PyTorch 风格的四遍 softmax(示意,不是可运行代码):

m = max(x, dim=-1)          # 第 1 遍:读 x,写 m
e = exp(x - m)              # 第 2 遍:读 x、m,写 e
s = sum(e, dim=-1)          # 第 3 遍:读 e,写 s
y = e / s                   # 第 4 遍:读 e、s,写 y

请:

  1. 用「tile 视角」把它改写成一段伪代码:显式写出每个中间量放在哪一级存储、有哪几次 全局访存。目标是把全局访存降到接近理论下界(读 x 一遍、写 y 一遍)。
  2. 说明你的改写为什么能成立(提示:softmax 是逐行的,每行可以独立处理;在线 softmax 的思想属于第 15 章,本章只需给出「行内 offline」的版本)。
  3. 指出你的方案里哪一步需要多缓冲或流水,哪一步需要同步。

要求:用伪代码,不要求 TileLang 语法;但必须显式标出每一块数据的存储位置。


E11分析难度 ★★无需硬件分析题 不需要硬件

给定 ,,fp16 输入、fp32 累加。假设你只能选择一对 block_M × block_K 与 block_K × block_N 的片上 tile。

  1. 写出一次「朴素无分块」实现在最坏情况下对 A 与 B 的总访存量(以字节为单位, 只算最外层的重复读取,忽略 cache)。
  2. 写出使用 128 × 32 的 tile 之后的理论最小访存量。
  3. 分别算出两种情况下的算术强度,并解释为什么分块能把一个「带宽不够」的问题变成 「算力够不够」的问题。
  4. 指出分块的物理代价:你为了多做几次浮点运算,付出的代价是什么?

E12概念难度 ★★无需硬件概念题 不需要硬件

本章 4.2 节说「需要被同时正确管理的东西变多了」。请:

  1. 列出至少 5 个在 v0.1.15 的昇腾后端里由编译器负责、而在 Ascend C 里要由程序员手工 维护的事项(可参考 _vendor/tilelang-v0.1.15/tilelang/ascend/README.md:144-152)。
  2. 解释为什么「把同步交给编译器」比「把 layout 交给编译器」更难,需要额外引入什么机制 (提示:社区文章提到了 SMT 求解器 Z3 与「issue interval 和 latency 必须分开」)。
  3. 从工程角度说明:为什么 TileLang@Ascend 团队把大量工程量花在验证上,而不是功能上。

E13综合难度 ★★★需要加速器

如果你有 NVIDIA GPU(sm90 及以上更佳):

  1. 安装锁定版本的 TileLang(安装步骤属于第 7 章,这里只需按官方 docs/get_started/Installation.md 操作)。
  2. 运行 _vendor/tilelang-v0.1.15/examples/gemm/example_gemm.py,打印 kernel.get_kernel_source(),找出生成的 CUDA 里哪一段对应你写的 T.copy、哪一段 对应 T.gemm。
  3. 只在同一台机器、同一 shape、同一 dtype、同一口径下对比:把 num_stages 从 3 改 成 1 和 4,记录每次 do_bench 的结果。写出你的测量命令与观察结论。

说明:本教程的写作环境没有 GPU,没有跑过这项实验;请以你自己机器的实测为准,并在结论里 写明测试条件。


E14综合难度 ★★★需要加速器

如果你有昇腾 NPU(Ascend 950,CANN 环境):

  1. 按 _vendor/tilelang-v0.1.15/tilelang/ascend/README.md 的 Quick Start 跑通 GEMM + ReLU 示例(shape 为 M=256, N=7168, K=2048)。
  2. 用 kernel.get_kernel_source() 导出生成的 PTO / Ascend C 源码,找出 asc_mmad (矩阵乘)与 MTE 搬运相关的调用。
  3. 把 T.Pipelined(..., num_stages=2) 改成 num_stages=3,记录是否仍然能通过编译 (提示:片上容量是硬约束);如果编译失败,把报错信息抄下来并解释原因。

说明:同样地,本教程没有 NPU,未运行过;请以你自己的环境为准。

章节自测

7 题 · 提交后本地评分

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

  1. 1. 本书把「机器平衡点」定义为峰值算力与峰值带宽的比值。它的单位是什么?

  2. 2. 按 §1.3 的口径,softmax(AI≈0.75)与 4096³ MatMul(AI≈1365)相比,正确的判断是哪一项?

  3. 3. 按 §2.3 的三要素定义,如果一块数据只有形状与存储位置、缺了「物理布局(归属)」,它退化成了什么?

  4. 4. 关于 NVIDIA 与昇腾两套存储层级的对应关系,下面哪一句与 §2.1 一致?

  5. 5. 按 §4.5 的三代对照表,Triton 留下的主要问题是什么?

  6. 6. TileLang 论文摘要说的「解耦」,究竟是把什么从什么里解耦出来?

  7. 7. 关于 v0.1.15 里三层接口的状态,下面哪一句与 §4.4 的表格一致?

本章代码

5 个文件

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

浏览本章全部代码 →

本章引用

源码链接指向锁定 tag

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

展开引用清单

第 01 章 参考资料

本章所有结论都基于下表中的本地资料或锁定版本源码。版本以 版本锁定 为准:TileLang v0.1.15 (commit a35f8ddf45eba16c21211ec8822d56ce5363036f)、TileLang-Ascend TileLang-ascend-v0.1.2.000-release(commit 89a9364e47a341fffdd65ed94786b1d4f9b6a78c)。 文中不出现「最新版」这类表述。

写作环境:macOS (Apple Silicon)、Python 3.11;没有 NVIDIA GPU,也没有昇腾 NPU。 因此本章所有代码示例的最高验证级别是「仅静态核对」(API 存在性与签名逐个在 _vendor/ 中比对过),没有任何一条性能数字来自本机实测。


锁定的源码引用(_vendor/tilelang-v0.1.15/)

id 引用位置 用途 核验方式与状态
S1 _vendor/tilelang-v0.1.15/tilelang/language/kernel.py:431-455 T.Kernel 的定义与 docstring(「a grid of tile programs」「target-neutral launch」) 已核验(2026-10-01):grep -n "^def Kernel" tilelang/language/kernel.py → 431;docstring 逐行阅读
S2 _vendor/tilelang-v0.1.15/tilelang/language/allocate.py:49 T.alloc_shared(shape, dtype, scope="shared.dyn") 签名与默认 scope 已核验(2026-10-01)
S3 _vendor/tilelang-v0.1.15/tilelang/language/allocate.py:81 T.alloc_fragment(shape, dtype, scope="local.fragment") 签名 已核验(2026-10-01)
S4 _vendor/tilelang-v0.1.15/tilelang/language/copy_op.py:59 T.copy 签名与「tile op 不直接展成 CUDA」的说明 已核验(2026-10-01)
S5 _vendor/tilelang-v0.1.15/tilelang/language/gemm_op.py:160 T.gemm(A, B, C, transpose_A, transpose_B, policy, clear_accum, annotations) 签名 已核验(2026-10-01)
S6 _vendor/tilelang-v0.1.15/tilelang/language/loop.py:13、:82、:112 T.Parallel、T.Persistent、T.Pipelined(num_stages=...) 签名 已核验(2026-10-01)
S7 _vendor/tilelang-v0.1.15/examples/quickstart.py 第 3 章正文引用的 tile 风格 GEMM + ReLU 示例 已核验(2026-10-01):文件存在,代码逐行比对;未运行(无 GPU)
S8 _vendor/tilelang-v0.1.15/examples/gemm/example_gemm.py 习题 E6/E13 引用的最小 GEMM 示例 已核验(2026-10-01);未运行
S9 _vendor/tilelang-v0.1.15/tilelang/cuda/pipeline.py:104-196 NV 侧 pass 顺序中 PipelinePlanning → InjectSoftwarePipeline → LayoutInference → LowerTileOp 的位置 已核验(2026-10-01)
S10 _vendor/tilelang-v0.1.15/tilelang/ascend/README.md:1-152 昇腾 950 后端定位、Quick Start(GEMM+ReLU)、与 Ascend C 的分工对照表 已核验(2026-10-01);未在 NPU 上运行
S11 _vendor/tilelang-v0.1.15/tilelang/ascend/language/allocate.py:26,37,46,55 T.alloc_l1 / alloc_l0a / alloc_l0b / alloc_l0c 的 scope 默认值 已核验(2026-10-01)
S12 _vendor/tilelang-v0.1.15/tilelang/ascend/language/copy_op.py:53-100 T.dual_copy 的语义(L0C→UB 硬件事务、M/N 拆分、对齐约束) 已核验(2026-10-01)
S13 _vendor/tilelang-v0.1.15/tilelang/ascend/language/frame.py:107,179 T.SimtVF(threads=128, latency=0)、T.SimdVF(latency=0) 已核验(2026-10-01)
S14 _vendor/tilelang-v0.1.15/tilelang/ascend/pipeline.py:108-114 昇腾侧的 EstimateLatency → AutoSchedule → PrepareMultiBuffer → InsertSync → MaterializeMultiBuffer 已核验(2026-10-01)
S15 _vendor/tilelang-v0.1.15/tilelang/language/eager/builder.py:1177 T.const(name, dtype) 已核验(2026-10-01)
S16 _vendor/tilelang-v0.1.15/tilelang/language/proxy.py:256 T.Tensor(TensorProxy) 已核验(2026-10-01)

「已核验」的含义:在锁定版本的工作树上用 grep -n 定位到定义行,并阅读了完整的签名与 docstring;行号区间与文件内容一致。这不是运行验证。


corpus 资料引用(资料库索引)

抓取日期:2026-09-30(Asia/Shanghai),见 corpus 的 资料库索引。

id 本地路径 上游链接 用途 核验状态
R1 TileLang: A Composable Tiled Programming Model for … https://arxiv.org/abs/2504.17577 TileLang 论文摘要:调度空间与数据流解耦、三层接口、composable 已核验(2026-10-01):本地文件为摘要全文,链接可达
R2 TileLang 官方文档 · 资料清单 https://tilelang.com/get_started/overview.html tile 的官方定义、三层接口(含 beginner 层未完成的说明)、显式存储分配 已核验(2026-10-01)
R3 Triton: An Intermediate Language and Compiler for T… https://www.eecs.harvard.edu/~htk/publication/2019-mapl-tillet-kung-cox.pdf ;DOI 10.1145/3315508.3329973 Triton 原始论文全文:tile 的定义、"statically shaped multi-dimensional sub-arrays"、三类编译器路线与 Triton 的取舍 已核验(2026-10-01):本地为逐页文本提取
R4 Halide documentation — a language for fast, portabl… https://halide-lang.org/ Halide 的「算法 / 调度分离」原则 已核验(2026-10-01)
R5 TVM: An Automated End-to-End Optimizing Compiler fo… https://arxiv.org/abs/1802.04799 TVM(OSDI 2018):Halide 路线在深度学习编译器中的延续 已核验(2026-10-01):本地为摘要
R6 TensorIR: An Abstraction for Automatic Tensorized P… https://arxiv.org/abs/2207.04296 TensorIR(ASPLOS 2023):block/axis IR,TileLang 的 IR 基础 已核验(2026-10-01):本地为摘要
R7 NVIDIA Hopper Architecture In-Depth https://developer.nvidia.com/blog/nvidia-hopper-architecture-in-depth/ H100:HBM3「over 3 TB/sec」、FP16 张量核 preliminary specs、TMA、thread block cluster、DSM、「first truly asynchronous GPU」、256KB shared+L1 / 最多 228KB shared 已核验(2026-10-01):本地为全文;厂商公开资料,非实测
R8 CUDA C++ Best Practices Guide https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html 「hundreds of clock cycles of memory latency」、算术指令延迟约 4 cycles(cc 7.0) 已核验(2026-10-01):本地为全文;引用处为第 3629、3145 行附近
R9 CUDA C++ Programming Guide https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html CUDA 的 grid/block/warp/thread 模型与存储层级 已核验(2026-10-01):本地为节选
R10 DeepSeek 开源昇腾基础组件将会带来哪些影响? https://www.zhihu.com/question/2088578683623814216/answer/2088624284100999121 昇腾 950 与 NV 的存储/执行/同步对照、Cube/AIC 与 Vector/AIV、Z3/SMT 调度、latency 与 issue interval 的区分 部分核验:本地为登录态抓取的全文;链接需登录才能访问;内容属社区一手文章,非厂商官方文档,其中的性能说法(如「性能与 Ascend C 打得有来有回」「代码行数差好几倍」)无公开基线与测量口径,本章只作为定性描述引用,未当作数字
R11 huawei-csl/pto-dsl https://github.com/huawei-csl/pto-dsl PTO-DSL 自述:对标 cuTile、暴露 L0/L1/UB、与 tilelang-ascend 的关系 已核验(2026-10-01)
R12 pto-spec/README.md https://github.com/PTO-ISA/pto-spec PTO 的形式化定义:Direct Tile operations 118 条、64 个 Tile 寄存器、execution mask 已核验(2026-10-01):本地为仓库工作树快照,版本为 0.58.7(开发分支),不是发布版;本章只引用其结构性描述
R13 TileSight: A First-Principles Tile-Centric Analytic… https://arxiv.org/abs/2607.22432 TileSight:以 tile 为中心的性能模型;MAPE 12.35%(A100/H200/B200/B6000)等 已核验(2026-10-01):本地为摘要;论文自述「will be open-sourced upon publication」,本章未使用其数字
R14 FlashAttention: Fast and Memory-Efficient Exact Att… https://arxiv.org/abs/2205.14135 FlashAttention:IO-aware tiling 的经典论证(BERT-large 15%、GPT-2 3×、LRA 2.4×) 已核验(2026-10-01):本地为摘要;数字来自论文自述,测量条件见其正文,本章未直接引用为 TileLang 的性能
R15 TileLang 知识体系与学习路径(编者笔记) (本书 corpus 内的综合文档,无独立上游链接) 知识体系与「TileLang 在软件栈里的位置」的框架 已核验(2026-10-01)

上游链接核验说明

  • 上表中标注「已核验」的链接,核验方式是:确认对应 corpus 文件在本地存在,且其头部记录了 抓取日期与原始 URL;本章未在写作时重新发起网络请求(corpus 抓取日期为 2026-09-30, 早于本章写作日期 2026-10-01,差异在 1 天内)。
  • R10(知乎)需要登录态才能访问,属 corpus 中唯一依赖登录的来源;本章只把其中的 架构性描述(Cube/AIC 与 Vector/AIV 的分工、存储角色、Z3 的作用)作为定性材料引用, 没有引用其中任何未经公开基线的性能结论。
  • R12(PTO 规范)引用的是开发分支工作树快照(version 0.58.7),不是某个已发布版本; 本章只使用了「tile 级指令集包含 Direct Tile operations 与 Tile 寄存器」这类结构描述。

关于性能数字的说明

本章出现的每一个数字都带来源与条件,汇总如下:

数字 来源 测量/口径条件
HBM3「over 3 TB/sec」、50 MB L2 R7 H100 SXM5,厂商公开资料
FP16 张量核密集 1000 TFLOPS R7 H100,厂商公开「preliminary performance specs」表,未实测
机器平衡点 ≈ 333 FLOP/Byte 本书推导,输入为 R7 的两个数字 峰值/带宽之比,未实测
未缓存 local/global 访存「数百个时钟周期」 R8 CUDA 编程模型,未缓存访存
多数算术指令延迟约 4 cycles R8 compute capability 7.0 设备
H100 每 SM 256 KB shared+L1,shared 可配到 228 KB R7 厂商公开资料,configurable
softmax/MatMul 的算术强度表 本书推导,公式见 1.3 节 理论最小访存量,fp16;表由 code/01-op-cost-model/op_cost_model.py 复算

没有来源的、或来自厂商宣传口径的数字一律未采用。 所有需要真实加速器才能得到的数字 (实测算力利用率、延迟、吞吐)在本章中均未出现。


待补资料

  1. cuTile 的一手资料缺失。 本章谱系图(图 5)里的 cuTile 节点只有 PTO-DSL 的间接 提及(R11),corpus 内没有 NVIDIA cuTile 的官方文档或仓库快照。建议后续版本补一条 https://docs.nvidia.com/cuda/cutile-python/ 的抓取。本章因此对 cuTile 只做了定位性 描述,没有展开。
  2. TileLang 论文的正文缺失。 corpus 里的 TileLang: A Composable Tiled Programming Model for … 只有摘要,正文 (尤其是评测章节的具体数字与图)不在本地。本章因此只引用了摘要中可直接核对的设计 主张。第 3 章若需要展开论文的评测数据,建议补抓正文 PDF 或 HTML 全量版本。
  3. TileLang 官方文档中「layout inference」的专页缺失。 _sources/compiler_internals/ 下只有 4 篇(letstmt_inline、inject_fence_proxy、tensor_checks、 metal_tilelang_development),没有 layout inference 的原理页。本章的 layout 描述来自 get_started/overview.md 与源码,第 21 章若需要更细的推导细节,可能需要补抓。

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