diff --git a/zh/appendix/debugging_warp_specialized.md b/zh/appendix/debugging_warp_specialized.md
index 5d9856db..ce57d261 100644
--- a/zh/appendix/debugging_warp_specialized.md
+++ b/zh/appendix/debugging_warp_specialized.md
@@ -1,10 +1,209 @@
----
-orphan: true
----
-
(chap_warp_spec_debug)=
# 调试 Warp-Specialized Kernel
-> 翻译状态:待翻译。对应英文章节:`appendix/debugging_warp_specialized.md`。
+{ref}`chap_gemm_advanced` 中的 GEMM Steps 7-9 会重叠 TMA load、`tcgen05` MMA 以及 TMEM/SMEM writeback。同样的调试方法也适用于 Flash Attention handoff:识别角色,识别每个角色拥有的 storage,然后用这个模型检查生成的 CUDA。
+
+不要一开始就重写 kernel。先确认运行环境有效,再检查生成的 CUDA。排除环境和编译期问题后,这类 kernel 的运行时失败通常都会归结为某个 broken handoff:未初始化的 barrier、错误 arrival count、藏在 role guard 里的 collective、过期 barrier phase,或者 producer 还没让写入可见就复用了 storage。
+
+## 调试 Kernel 之前
+
+先排除运行时上下文问题:
+
+```bash
+python -c "import tvm, tvm.tirx; print(tvm.__file__, tvm.__version__)"
+python -c "import torch; print(torch.cuda.get_device_name(), torch.cuda.get_device_capability())"
+```
+
+这些 kernel 目标是 Blackwell(`sm_100a`)。如果 Python import 了过期 TVM checkout,或者 GPU 不是 Blackwell 级别,先修复这些问题再改 kernel。然后在看性能之前,先运行 kernel 最小的 correctness check,例如 `run_correctness()`。
+
+## 调试流程
+
+1. 用仍会失败的最小 shape 复现问题。如果 failure 是 illegal memory access,下一次运行前重启 Python。
+2. 如果 compilation fails,先检查安装的 API、target、`dispatch=` 和 buffer scope,再阅读 runtime synchronization 代码。
+3. 保存 `inspect_source("cuda")` 输出。在重新读 Python 之前,先搜索 role guard、`mbarrier_init`、`tcgen05`、`cp.async.bulk.tensor` 和 `cta_sync()`。
+4. 为失败的 kernel path 写出 roles / storage / handoff / lifetime 表。
+5. 用这张表检查生成的 CUDA:barrier init 是否位于 role branch 前;TMA producer、MMA issuer、writeback group 是否符合预期;warpgroup-only branch 内是否没有 CTA-wide collective。
+6. 把运行归类为 deadlock、crash、wrong result,或 correct-but-slow,然后使用下面对应小节。
+7. 一次只改一个 handoff:init count、arrive/wait phase、role guard、fence、TMA store drain、TMEM alloc/dealloc 或 tile-scheduler advance。
+8. 重新测性能前先重新跑 correctness。
+
+## 需要记录什么
+
+对任何异步 kernel,改代码前先做一个小 worksheet:
+
+| 项目 | 需要写下什么 |
+|---|---|
+| Roles | 发起每个 async operation 的精确线程、warp、warpgroup 或 CTA。 |
+| Storage | 每个 tile 在每一步的 live 位置:GMEM、SMEM、TMEM 或 registers。 |
+| Handoff | Producer、consumer、signal object、arrival count、phase,以及让数据可见的 fence 或 drain。 |
+| Lifetime | 每个 storage slot 最早何时可以被复用、读回或释放。 |
+
+然后用 worksheet 检查生成的 CUDA:
+
+- Role guard 与 roles 表匹配。
+- Barrier init 出现在 guarded role branch 之前。
+- Collective operation 没有被 lane、warp 或 warpgroup guard 意外缩窄。
+- Arrive/wait phase 与 handoff 表匹配。
+- TMA store drain、TMEM dealloc 和 SMEM reuse 只在 lifetime 表允许之后发生。
+
+同一张 worksheet 可以用于 TMA->MMA->writeback GEMM pipeline,也可以用于 Flash Attention 中的 score/softmax/value/correction handoff。
+
+## 如果编译失败
+
+先修复 compile-time failure,再调试 runtime synchronization:
+
+| 症状 | 可能区域 | 首先检查 |
+|---|---|---|
+| Unknown TIRx API 或 attribute error | 安装的 wheel 与教程代码不匹配 | 打印 `tvm.__file__` 和 `tvm.__version__`;用 {ref}`chap_language_reference` 对比 API 名称。 |
+| Unsupported `dispatch=` | 选中的 target 或 primitive 不支持该路径 | 检查 `dispatch` 参数和 target capability;本教程中的 `tcgen05` 路径需要 Blackwell。 |
+| Buffer scope mismatch | Buffer 正在通过错误硬件路径使用 | 检查 worksheet 中的 storage 行:TMEM 必须通过 `tcgen05` 访问,TMA operand 必须使用兼容的 GMEM/SMEM layout。 |
+| 编译成功但生成 CUDA 缺少预期路径 | Dispatch 没有按预期 lower | 改算法前先检查生成 CUDA 中是否有 `tcgen05` 和 `cp.async.bulk.tensor`。 |
+
+## 检查生成代码
+
+对任何已编译 kernel,保存 CUDA,方便搜索和 diff:
+
+```python
+from pathlib import Path
+
+cuda_source = ex.mod.imports[0].inspect_source("cuda")
+Path("artifacts").mkdir(exist_ok=True)
+Path("artifacts/my_kernel.cu").write_text(cuda_source, encoding="utf-8")
+print(cuda_source)
+```
+
+生成代码中 TIRx construct 到 CUDA 的映射如下:
+
+| TIRx | Generated CUDA |
+|------|---------------|
+| `wg_id == 0` | `(warp_id_in_cta >> 2) == 0` |
+| `wg_id == 1` | `(warp_id_in_cta >> 2) == 1` |
+| `warp_id == 0` | `(warp_id_in_cta & 3) == 0` |
+| `warp_id == 3` | `(warp_id_in_cta & 3) == 3` |
+| `lane_id == 0` | `(((int)threadIdx.x) % 32) == 0` |
+| `.init()` internal guard | `((int)threadIdx.x) < 1`(仅 CTA thread 0) |
+| `elect_sync()` | `tvm_builtin_elect_one_sync_op()` |
+
+读完整 kernel 前先扫描这些字符串:
+
+| Generated CUDA | 检查 |
+|---|---|
+| `if (threadIdx.x < 1)` | 单个 CTA-thread guard,常用于 barrier initialization |
+| `mbarrier_init` | Barrier initialization 存在,并出现在 role branch 之前 |
+| `tcgen05` | Tensor Core 路径已生成 |
+| `cp.async.bulk.tensor` | Copy lowered 到 TMA |
+| `cta_sync();` | CTA-wide barrier;它不能位于 `wg_id` branch 内 |
+
+## Step 7 参考骨架
+
+正确编译的 Step 7 kernel 有如下 top-level 形状。下面的 guard 用 role name 写出以便阅读;在生成 CUDA 中,请搜索上表中的对应表达式。
+
+```c
+// (1) Barrier inits: top level, CTA thread 0 only
+if (threadIdx.x < 1) {
+ mbarrier_init(tma2mma[0..1], 1);
+ mbarrier_init(mma2tma[0..1], 1);
+ mbarrier_init(mma2ld, 1);
+ mbarrier_init(ld2mma, 128); // arrived by all 128 WG0 threads
+}
+
+// (2) TMEM alloc: WG0 warp 0, all lanes of the issuing warp
+if (wg_id == 0 && warp_id == 0) tcgen05_alloc(..., 512);
+
+// (3) Fences + cta_sync, then phase init: producer=1, consumer=0
+
+// (4) Warp-specialized loop
+if (wg_id == 1 && warp_id == 3 && elect_sync) { /* TMA */ while(valid){ ... next_tile(); } }
+if (wg_id == 1 && warp_id == 0 && elect_sync) { /* MMA */ while(valid){ ... next_tile(); } }
+if (wg_id == 0) { /* WB */ while(valid){ ... next_tile(); } }
+
+// (5) Cleanup: issuing warp, no lane guard
+cta_sync();
+if (warp_id == 0) { tcgen05_relinquish_alloc_permit(); tcgen05_dealloc(..., 512); }
+```
+
+改算法前先检查这些点:
+
+- Barrier init 位于 top level,而不是 `wg_id` guard 内。
+- `tcgen05_alloc` 和 `tcgen05_dealloc` 有 warp guard,但没有 lane guard;issuing warp 的所有 lane 都参与。
+- TMA 和 MMA loop 都迭代 `K_TILES` 次。
+- Phase init 是 producer=`1`,consumer=`0`。
+
+## 症状映射
+
+从症状开始,但把它当作线索,而不是最终诊断:
+
+| 线索 | 可能区域 | 首先检查 |
+|---|---|---|
+| Kernel hang,随后 runtime 报 unspecified launch failure | Deadlock | Barrier init 位置、arrival count、`cta_sync()` 位置和 `next_tile()` 参与情况 |
+| Illegal memory access、XID,或后续无关 CUDA 调用也失败 | Crash / poisoned context | 重启 Python,然后检查 pointer range、storage lifetime 和 collective participation |
+| 错误 row 以 128-row 或 tile-sized stripe 出现 | Sync race 或 tile-index mismatch | Producer/consumer phase、scheduler advance,以及哪个 warpgroup 拥有每个 row stripe |
+| `NaN` 或明显 invalid values | Descriptor、operand setup 或未初始化 accumulation | SMEM/TMEM descriptor setup、swizzle/layout 和 accumulator initialization |
+| 有限但带 pattern 的错误值 | Stale 或部分可见数据 | 缺少 fence、缺少 TMA store drain,或 storage 在 lifetime 表允许前被复用 |
+| 输出正确但没有预期 speedup | Dispatch 或 resource 问题 | 生成 CUDA 路径、pipeline depth、occupancy 和 register spill |
+
+## 何时重启 Python
+
+CUDA error 不一定会自动清理状态。发生 illegal memory access、XID 或 “CUDA context poisoned” 错误后,后续无关调用如 `torch.randn` 可能继续失败。测试下一个修复前重启 Python process,否则你可能在调试上一次 crash,而不是当前代码。
+
+## Deadlock
+
+按顺序检查这些点:
+
+- **Arrival count 与 init count 不匹配。** 常见情况:`MBarrier.init(128)`,但 `arrive` 被 `if warp_id == 0: if lane_id == 0:` guard 住,于是只有 1 个线程 arrive,wait 永远不返回。
+
+ | Barrier | init(count) | Who arrives | Arrivals |
+ |---|---|---|---|
+ | `TMABar` (tma->mma) | 1 | TMA engine 通过 `arrive(stage, bytes)` | 1 |
+ | `TCGen05Bar` (mma->tma, mma->ld) | 1 | MMA warp 通过 `tcgen05.commit` | 1 |
+ | `MBarrier` (ld->mma) | 128 | 所有 WG0 线程通过 `arrive` | 128 |
+
+- **Barrier init 嵌在 `wg_id` guard 内。** `.init()` 会 lower 成 `if threadIdx.x < 1:`,也就是 CTA thread 0。CTA thread 0 位于 WG0,所以 `if wg_id == 1:` 会阻止所有线程执行 init。Init 必须位于 top level;用 `inspect_source()` 中的 `grep mbarrier_init` 验证。
+
+- **`cta_sync()` 位于 warpgroup branch 内。** `cta_sync` 是 `__syncthreads()`,要求所有 CTA 线程到达。放在 `if wg_id == 0:` 内时,WG1 永远不会到达。单个 warpgroup barrier 请使用 `T.cuda.warpgroup_sync(10)`。
+
+- **`tile_scheduler.next_tile()` 被某些 consumer-warpgroup thread 跳过。** Scheduler 跟踪 per-thread state;跳过它的线程可能永远循环。
+
+- **TMA 和 MMA 的 K-tile count 不一致。** 如果 MMA 做 `K_TILES - 1` 而不是 `K_TILES`,barrier phase 会 drift,第二个 outer tile 可能 deadlock。
+
+- **`PipelineState` 初始 phase 错误。** Producer 从 `phase=1` 开始,使第一次 wait 通过;consumer 从 `phase=0` 开始,使第一次 wait 阻塞。如果两者从同一个 phase 开始,第一次 handoff 就可能立即 deadlock。
+
+## Crash 和 Context Poisoning
+
+常见原因:
+
+- **`pool.commit()` 之后又 `pool.alloc`。** Barrier wrapper 内部会调用 `alloc`。正确顺序是:`tmem_addr -> barrier wrappers -> move_base_to(1024) -> Asmem / Bsmem / Dsmem -> commit()`。
+- **`tcgen05.alloc` 或 `tcgen05.dealloc` 带 lane guard。** Issuing warp 必须所有 lane 都参与。`if lane_id == 0:` 只运行一个线程,属于 undefined behavior。
+- **`tcgen05.dealloc` 前缺少 `cta_sync()`。** TMEM 在 writeback 仍在读取时被释放。
+- **GMEM 或 SMEM 越界访问。** 缩小到一个 tile,检查 scheduler 的 `m_idx` / `n_idx`,并检查当前 shape 是否是 kernel 的 tile 或 cluster tile 的倍数。
+
+## 错误结果
+
+猜测前先按 pattern 分类错误输出。整条 row stripe 通常指向 producer/consumer phase、tile-index 或 role-ownership mismatch。`NaN` 输出通常指向 descriptor setup、operand setup 或未初始化 accumulation。有限但带 pattern 的错误值通常意味着 consumer 读取了旧 tile、部分写入的 tile,或者 store 还没 drain 的数据。
+
+- **`tcgen05.commit` 在 `elect_sync` 外。** 所有 32 个线程都会创建 commit group;其中 31 个空 group 会立即 signal mbarrier。TMA 可能在 MMA 读取 SMEM 前覆盖它。
+- **TMA store 前缺少 `fence.proxy_async("shared::cta")`。** TMA engine 可能看不到线程对 SMEM 的写入。
+- **TMA store 后缺少 `cp_async.bulk.commit_group()` 加 `wait_group(0)`。** 下一 tile 可能在 store drain 之前复用 Dsmem。
+- **Persistent kernel 在 1024x1024 等小尺寸上间歇失败。** 大尺寸可能用更长 K-loop 掩盖 race。重新检查 tile 之间的 phase reset 和 TMA-store commit/wait。
+- **`fence.after_thread_sync()` 通常不是修复。** MMA-completion mbarrier 已经携带 release-acquire 语义。Steps 8 和 9 只在 writeback edge 上保守添加它,也就是 `mma2ld.wait` 之后、第一个 `tcgen05.ld` 之前;不要在 TMA-to-MMA edge 上例行添加。
+
+## 正确但很慢
+
+如果输出正确但性能远低于预期,使用同样的 inspection loop:
+
+| 线索 | 可能区域 | 首先检查 |
+|---|---|---|
+| 生成 CUDA 没有 `cp.async.bulk.tensor` | Copy 没有 lower 到 TMA | 检查 `dispatch="tma"`、target capability 和 operand layout |
+| 生成 CUDA 没有 `tcgen05` path | MMA 没有 lower 到 Blackwell Tensor Core 指令 | 检查 `dispatch="tcgen05"`、target capability 和 operand layout |
+| TMA 和 MMA 没有重叠 | Pipeline 太浅或 phase 串行化了 producer/consumer | 检查生成 CUDA 中 wait/arrive/advance 的顺序 |
+| 小 shape correctness 好,但大 shape 很慢 | Register spill、occupancy 或 staging-buffer pressure | 检查编译器 resource report;减小 tile size、chunk writeback 或降低 pipeline depth |
+
+## 提交高质量 Issue
+
+如果 failure 在上述检查后仍然存在,请先 reduce,再到 [Apache TVM GitHub 仓库](https://github.com/apache/tvm/issues)提交 issue。请包含:
-本页用于放置调试 warp-specialized kernel 的中文翻译。
+- `tvm.__file__` / `tvm.__version__` 输出和 GPU capability;
+- 能复现 failure 的最小 shape;
+- failure 是 compile-time、deadlock、crash、wrong result,还是 correct-but-slow;
+- 最小 kernel 或 notebook cell,以及对应 correctness check;
+- 保存的 `inspect_source("cuda")` 输出,或能显示可疑 guard、barrier、dispatch path 的最小摘录。
diff --git a/zh/appendix/index.md b/zh/appendix/index.md
index 3fd23d8f..a95227d3 100644
--- a/zh/appendix/index.md
+++ b/zh/appendix/index.md
@@ -1,10 +1,16 @@
----
-orphan: true
----
-
(chap_appendix)=
# 参考资料
-> 翻译状态:待翻译。对应英文章节:`appendix/index.md`。
+主线内容贯穿第一部分到第四部分。参考资料收录的是阅读过程中需要随时查阅的材料:
+
+| 需求 | 位置 |
+|------|-----|
+| 查询 TIRx 语言特性 | **{ref}`chap_language_reference`** |
+| 编译器内部机制(lowering pipeline) | **{ref}`chap_arch`** |
+| 调试异步 GEMM/FA 的 hang、crash、错误结果或性能问题 | **{ref}`chap_warp_spec_debug`** |
+
+完整的 `tvm.tirx` Python API 请参阅
+[上游 TVM 文档](https://tvm.apache.org/docs/)。
-本页用于放置参考资料入口的中文内容。
+TIRx 原生层({ref}`chap_tirx_primer`)和 tensor layout 模型
+({ref}`chap_tirx_layout_api`)在第二部分介绍。
diff --git a/zh/chapter_async_barriers/index.md b/zh/chapter_async_barriers/index.md
index 6d6660a2..1d22208b 100644
--- a/zh/chapter_async_barriers/index.md
+++ b/zh/chapter_async_barriers/index.md
@@ -1,52 +1,94 @@
(chap_async_barriers)=
# 异步协作:mbarrier
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- TMA 和 Tensor Core 都是异步的,所以“发起工作”和“工作完成”不是一回事;消费者需要显式的完成信号。
+- `mbarrier` 就是这个信号:producer arrive,consumer wait,并且它会跟踪 arrival count 以及 TMA 使用的 byte count。
+- 每个 barrier 都带有一个 *phase*,每完成一轮就翻转一次;consumer 等待正确的 phase,才能安全地越过这个同步点。
:::
-TODO:翻译导言部分。
+TMA({ref}`chap_tma`)和 Tensor Core({ref}`chap_tensor_cores`)操作都是异步的。当 kernel 发起 TMA load 或 `tcgen05` MMA 时,发起线程不会等待操作完成。指令只是被提交给硬件引擎;真正的数据搬运或矩阵运算会继续与程序的其他部分并行执行。
+
+这很有用,因为它允许内存搬运和计算重叠。但这也意味着,单靠程序顺序无法证明数据已经准备好。后面的指令可能会在前面的异步操作完成之前运行。如果 TMA 仍在写 shared-memory tile 时 MMA 已经开始读取它,MMA 就会读到不完整的数据。如果 epilogue 在 Tensor Core 写完 accumulator 之前读取 TMEM,它会读到错误值。如果 kernel 等错了条件,它甚至可能永远无法继续前进。
+
+因此,kernel 在每一个异步交接点都需要显式完成信号。`mbarrier` 就是这个信号。Producer 在工作完成时 arrive 到 barrier,consumer 在使用产物之前 wait 这个 barrier。同一个机制可以用于 TMA 到 MMA 的交接、MMA 到 epilogue 的交接,以及 pipeline stage 中的 buffer 复用。
+
+Barrier 不只是一次性的 flag。它带有一个 phase bit,并且每当 barrier 完成一轮 arrival,这个 phase bit 都会改变。Phase 让同一个 barrier 可以在很多 loop iteration 中复用,而不会把一次 iteration 的完成误认为另一次 iteration 的完成。
## mbarrier
-TODO:翻译 “The mbarrier” 小节。
+`mbarrier` 是 memory barrier 的缩写,是存放在 shared memory 中的硬件同步对象。概念上,它包含两部分状态:arrival counter 和 phase bit。Counter 告诉 barrier 当前轮还缺多少次 arrival;phase bit 告诉 kernel 当前 barrier 处于哪一轮。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:620px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
```
+*交互图:`mbarrier` 的状态视图,展示 arrival counter、phase bit,以及 `init`、`arrive` 和 `wait` 操作;点击字段可以聚焦查看。*
+
+Barrier 从初始化开始。在 `init` 期间,kernel 设置这个 barrier 期望收到多少次 arrival。Barrier 从 phase 0 开始,并把 counter 设置为期望的 arrival count。从这之后,barrier 会等待所有需要的 producer 或资源使用者报告自己已经完成。
+
+一次 arrival 会减少 barrier 仍在等待的工作量。Kernel 的不同部分可以用不同方式 arrive 到 barrier,这些差异很重要。
+
+对于 TMA load,常见的 arrival 路径是 tx-count arrival。像 `mbarrier.arrive.expect_tx(bytes)` 这样的操作会做两件事。第一,它算作发起线程在 barrier 上的一次 arrival。第二,它记录 TMA 引擎预计要传输的 byte 数。Barrier 不会因为发起线程已经 arrive 就完成。它还会等待 TMA 引擎在传输完成时把 byte count 清零。只有两个条件都满足时 phase 才会翻转:普通 arrival count 到达 0,并且 pending tx byte count 也到达 0。
+
+因此,不应该把 `expect_tx` 理解成“多一次普通 arrival”。它是在为异步 copy 设置一个 byte 预算。硬件随后通过 complete-tx 更新来记录真实 copy 的完成情况。只有 arrival 和 byte transfer 都完成之后,barrier 才完成。
+
+对于 Tensor Core 工作,arrival 路径不同。`tcgen05` MMA 不会因为 MMA 被发起就自动推进一个 barrier。Kernel 必须显式把 barrier arrival 绑定到 commit 路径上,例如通过 `tcgen05.commit.mbarrier::arrive` 操作。当被 commit 的 group 完成时,Tensor Core 侧会执行 barrier arrival。如果 kernel 忘了这个 commit arrival,等待 barrier 的 consumer 就会永远等待下去。
+
+普通线程也可以直接 arrive 到 barrier。当普通线程代码是 producer,或者一组线程要宣布它已经使用完某个资源时,就会用到这种方式。例如,consumer 读完一个 shared-memory buffer 后,可以 arrive 到一个 barrier,用来告诉 producer 这个 buffer 可以复用了。
-*点击图中组件查看细节:TODO:翻译 mbarrier mechanism 图注。*
+等待是同一个协议的 consumer 侧。Consumer 会等待 barrier 完成当前 iteration 所期望的 phase。只有这样,读取数据或复用这个 barrier 保护的资源才是安全的。
+
+关键点是:异步硬件不仅会跑在程序前面,它还会通过 barrier 把完成事件报告回来。TMA 可以发出 shared-memory tile 已经就绪的信号。Tensor Core 工作可以发出 TMEM 结果已经就绪的信号。普通线程可以发出某个 buffer 不再被使用的信号。Barrier 把这些情况统一成 producer-consumer 形状:producer arrive,consumer wait。
## Phase Tracking
-TODO:翻译 “Phase Tracking” 小节。
+Barrier 通常不会只分配给一次使用。一个 pipelined K-loop 可能会执行同一个交接数百次;如果每次 iteration 都分配一个新的 shared-memory barrier,并不现实。相反,kernel 会保留一小组固定 barrier,并在 loop 前进时复用它们。
+
+Phase bit 让这种复用变得安全。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
```
+*交互图:多个 pipeline iteration 复用同一个 barrier,展示每一轮完成后 phase bit 如何翻转。*
+
+每当 barrier 完成当前轮的全部 arrival,它都会翻转 phase:phase 0 变成 phase 1,phase 1 变成 phase 0,如此循环。Wait 操作会检查 consumer 期望的 phase。这个期望 phase 由 kernel 保存在寄存器中。某个 stage 成功等待一轮之后,kernel 会在下一轮使用这个 barrier 前翻转自己的本地 phase 值。
+
+这可以防止 kernel 把旧完成误认为新完成。假设一个 barrier 被用于某次 TMA load,并且已经完成。如果下一次 loop iteration 复用同一个 barrier 但没有跟踪 phase,那么 consumer 可能看到前一次完成,并错误地认为新的 load 已经就绪。Phase bit 把这两轮区分开。Iteration 0 等待一个 phase,iteration 1 等待相反的 phase,iteration 2 再等待第一个 phase,这个模式持续下去。
+
+在真实 pipeline 中,记录通常按 stage 进行。Kernel 有固定数量的 shared-memory stage、匹配数量的 barrier,以及一小组保存在寄存器中的 phase 值。Loop 前进时,每个逻辑 iteration 映射到一个物理 stage,而 phase 值告诉 wait 操作它正在等待这个物理 barrier 的哪一轮。
+
+这就是为什么后面的 GEMM 代码不需要每个 K tile 一个 barrier({ref}`chap_gemm_async`)。它需要每个可复用 stage 一个 barrier,再加上 phase tracking。Stage index 选择 shared-memory buffer 和 barrier。Phase 值把当前对这个 stage 的使用与上一次使用区分开。
-*点击图中组件查看细节:TODO:翻译 phase tracking 图注。*
+**Try with your agent**:给它一个 two-stage pipeline,并让它 trace 四次 iteration。对每次 iteration,列出 stage index、本地 phase 值、barrier 何时翻转,以及如果 stage 复用前没有翻转 phase 会出什么问题。
## 同步规则
-TODO:翻译 “Synchronization Rules” 小节。
+理解 barrier 和 phase 机制之后,tensor-core kernel 中的同步模式就相当机械了。每当一条路径产生数据,或者释放另一条路径将要消费的资源时,这个交接都必须显式表达。
+
+常见情况有三类。
+
+第一类是线程代码为异步引擎生产数据。如果线程写 shared memory,而后续 TMA store 或 MMA 指令要读这个 shared memory,那么 kernel 必须先让线程写入对引擎可见。这需要合适的 thread-level synchronization 或 fence。具体指令取决于交接 scope,但原因始终相同:engine 不能在 producing threads 写完之前观察这个 shared-memory buffer。
+
+第二类是 TMA 为 MMA 生产数据。TMA load 会异步填充一个 shared-memory tile。MMA 路径不能因为 TMA 指令已经发起就推断 tile 已经就绪。TMA 操作必须关联一个 `mbarrier`,MMA 路径必须在读取 tile 之前等待这个 barrier。
+
+第三类是 MMA 为 epilogue 生产数据。`tcgen05` MMA 会异步地把结果写入 TMEM。Epilogue 不能在 Tensor Core 完成相关工作之前安全读取 accumulator。因此,MMA commit 路径会 arrive 到一个 completion barrier,而 epilogue 在读取 TMEM 之前等待这个 barrier。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:700px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
```
+*交互图:TMA load 通过 `mbarrier` 发出完成信号。MMA 路径在读取 shared-memory tile 之前等待 barrier。Tensor Core 到 epilogue 的交接也是同一个形状,只是 arrival 来自 Tensor Core commit 路径,而不是 TMA。*
+
+同一个思想也适用于资源复用。Barrier 不只是 data-ready 信号,也可以是“资源已经空闲”的信号。一个 shared-memory stage 不能在旧 tile 的所有 consumer 都完成之前被覆盖。一个 TMEM 区域不能在上一个使用者读写完成之前被复用。在这些情况下,arrival 表示“我已经用完这个资源”,wait 表示“现在可以安全地为下一个 stage 复用这个资源”。
-*点击图中组件查看细节:TODO:翻译 mbarrier TMA timeline 图注。*
+这也是阅读 pipelined GEMM kernel 中同步逻辑的正确方式。Wait 和 arrive 并不是作为防御性编程到处散落。每一个都标记了一次具体的 ownership transfer:tile 变得可读、accumulator 变得可读,或者 buffer 变得可复用。一旦识别出这些交接,控制流就会容易跟踪得多。
diff --git a/zh/chapter_clc/index.md b/zh/chapter_clc/index.md
index 739f05f1..65400086 100644
--- a/zh/chapter_clc/index.md
+++ b/zh/chapter_clc/index.md
@@ -1,31 +1,72 @@
(chap_clc)=
# 进阶:Cluster Launch Control
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- Persistent kernel 会让固定数量的 CTA 或 CTA cluster 驻留在 GPU 上(通常规模接近每个 SM 一个 active work owner,但不依赖严格的 1:1 映射),并让它们循环处理多个 output tile,而不是每个 tile 启动一个 CTA。
+- Cluster Launch Control 是 Blackwell 的硬件机制,它让一个驻留中的 cluster 可以在运行时请求另一个 tile。它是一条硬件 work-stealing 路径,围绕两条 PTX 指令构建:一条指令请求 work,另一条指令读回请求是否成功。
+- 主要收益是改善 tail 行为。当 tile 开销不均匀,或者 tile 数量不能被可用 SM 均匀整除时,提前完成的 CTA 可以继续拉取更多 work,而不是空等。
:::
-TODO:翻译导言部分。
+Persistent GEMM 不会把 CUDA grid 当作固定的 “one CTA per output tile” launch。相反,它启动较小数量的长生命周期 CTA 或 CTA cluster。每个 CTA/cluster 计算一个 tile,然后前进到另一个 tile,再继续计算,直到输出空间完成。这正是 {ref}`chap_gemm_advanced` 中逐步构建出的执行模式。
+
+一旦 kernel 变成 persistent,主要调度问题就很简单:当一个 CTA 或 cluster 完成当前 tile 后,下一个 tile 从哪里来?
+
+最简单的答案是静态公式。例如,kernel 可以根据 CTA id 计算 tile coordinate,然后按 grid stride 前进。这很容易实现,并且当所有 tile 开销大致相同、tile 数量在 GPU 上分布均匀时效果很好。但这个 schedule 在 work 真正运行之前就已经决定好了。如果少数 tile 更慢,或者最后几个 tile 分配不均匀,有些 SM 会提前完成自己的份额,而另一些 SM 仍在处理 tail。
+
+Cluster Launch Control,简称 CLC,会改变这个调度模型。Persistent cluster 不再预先决定完整分配,而是可以向硬件 grid scheduler 请求另一个尚未 launch 的 cluster 的 work。如果请求成功,当前 cluster 会接管那个 cluster coordinate,并计算对应 tile。如果请求失败,就说明没有更多 work 可以 steal,loop 退出。
+
+这和 thread block cluster 本身不是一回事。Thread block cluster(一起 launch 的 CTA,带有 cluster-level synchronization,并能访问 distributed shared memory)是在 Hopper 引入的({ref}`chap_background`)。CLC 是 Blackwell 新增的机制,它让这些 cluster coordinate 上的调度变成动态的。Cluster 已经是 launch 的单位;CLC 让一个已经运行的 cluster 可以取消一个 pending launch,并继承它的 coordinate。
## 两条指令
-TODO:翻译 “The Two Instructions” 小节。
+Cluster Launch Control 通过两条 PTX 指令暴露。第一条指令向 grid scheduler 发送一个异步请求。第二条指令读取响应。
+
+请求指令是 `clusterlaunchcontrol.try_cancel.async`。
+
+一次 `try_cancel` 会请求 scheduler 取消一个 pending cluster 的 launch,并把那个 cluster 的 coordinate 返回给调用方。响应会作为一个 16-byte record 写入 shared memory。由于请求是异步的,指令不会等待响应到达。相反,完成事件通过 `mbarrier` 报告,使用的还是 TMA 中同样的 barrier-and-phase 模型。
+
+这个细节很重要,因为它意味着 CLC 不会引入新的等待模型。Kernel 发起请求,把它关联到一个 barrier,随后在读取响应之前等待这个 barrier。响应到达通过带 byte-count completion 的 barrier 发出信号,整体风格和其他异步硬件操作一致(见 {ref}`chap_async_barriers`)。
+
+Barrier 触发后,kernel 使用 query 指令。
+
+第一个 query 是 `clusterlaunchcontrol.query_cancel.is_canceled`。它返回一个 predicate,告诉 kernel cancellation 是否成功。True predicate 表示 scheduler 找到了一个 pending cluster launch,取消了它,并返回了它的 coordinate。False predicate 表示没有 pending work 可以拿了。
+
+只有当 `is_canceled` 为 true 时,kernel 才应该读取 coordinate。读取使用 `clusterlaunchcontrol.query_cancel.get_first_ctaid`,它会提取被取消 cluster 的第一个 CTA id。这个 CTA id 是一个 coordinate vector,通常读作 `(x, y, z)`,kernel 会把它解码成下一步要计算的 output tile。
+
+这个协议中没有数值形式的 sentinel tile id。Kernel 根据 predicate 分支。如果 predicate 为 true,coordinate 有效。如果 predicate 为 false,work-stealing loop 完成。
+
+从底层看,这个形状直接来自 CLC 的实际行为。硬件不是从软件队列里分配一个抽象 task。它取消的是一个尚未发生的 cluster launch。因此,成功响应包含一个真实的 cluster coordinate。失败响应只是意味着 launch queue 已经耗尽。
## Work-Stealing Loop
-TODO:翻译 “The Work-Stealing Loop” 小节。
+有了这两条指令,persistent scheduler 就变成一个短循环。
+
+在循环中的任意时刻,cluster 都负责计算一个 tile。在开始这个 tile 之前,它会为可能的下一个 tile 发送一个 `try_cancel` 请求。请求异步运行。当 scheduler 处理这个请求时,cluster 计算当前 tile。
+
+当前 tile 完成后,cluster 等待与 `try_cancel` 响应关联的 `mbarrier`。响应就绪后,它调用 `query_cancel.is_canceled`。如果 predicate 为 true,它调用 `query_cancel.get_first_ctaid`,解码返回的 coordinate,并把它作为下一个 tile。如果 predicate 为 false,就说明没有 work 了,cluster 退出。
+
+代码形状是:
+
+1. 为可能的下一个 tile 发起 `try_cancel`;
+2. 在请求飞行期间计算当前 tile;
+3. 等待响应 barrier;
+4. 查询 cancellation 是否成功;
+5. 要么用返回的 coordinate 继续,要么退出。
+
+请求放置的位置让这个循环有价值。Cluster 不是等当前 tile 完成之后才请求更多 work。它先请求,再计算。这样 scheduler 请求就与有用计算重叠。当前 tile 完成时,下一个 tile 的答案往往已经可用。
+
+这和 persistent kernel 在其他位置使用异步 copy 与 tensor-core barrier 的原因相同。Kernel 会避免把长延迟操作直接放在关键路径上。CLC 把同样的思想应用到 tile scheduling:提前请求下一份 work,计算当前 work,然后在需要时消费调度结果。
## 与 Persistent GEMM 的关系
-TODO:翻译 “Relation to Persistent GEMM” 小节。
+{ref}`chap_gemm_advanced` 中的 persistent GEMM 在主要讲解里使用静态 scheduler。静态 scheduler 更容易解释,因为下一个 tile 可以直接从 loop state 计算出来。例如,`ClusterPersistentScheduler2D` 这样的 scheduler 可以用 output tile 空间上的 grid-stride pattern 分配 tile。
+
+CLC 是这种静态分配的动态替代品。外层 loop 保持不变:每个驻留 cluster 反复计算一个 output tile,然后前进到另一个。变化的是下一个 tile 从哪里来。使用静态 scheduler 时,下一个 tile 由公式计算。使用 CLC 时,下一个 tile 由硬件 work stealing 返回。
+
+这个差异在 launch 尾部最明显。在静态 schedule 中,剩余 work 可能分布不均。有些 SM 可能已经没有分配到的 tile,而另一些 SM 仍然还有几个 tile 要做。使用 CLC 时,提前完成的 cluster 会请求另一个 pending cluster coordinate。只要 launch queue 中还有 work,提前完成者就能继续拉取更多 tile。
+
+当 tile 开销不均匀时,这也很重要。有些 GEMM tile 可能因为边界、masking、sparsity、grouped scheduling,或者主矩阵乘法周围的 fused work,而走不同路径。静态 schedule 假设 tile 分配在任何成本被观察到之前就足够好。CLC 不需要这个假设。它只在 cluster 变得可用之后才分配更多 work。
+
+因此,在 TIRx 中,CLC 可以暴露成一个动态 tile scheduler。编程模型不需要改变单个 tile 的计算。Tile body 仍然是静态 scheduler 使用的同一个 persistent GEMM body。Scheduler 从“用公式计算我的下一个 tile coordinate”变成“向硬件请求下一个可用 cluster coordinate”。结果仍然是同一个 persistent loop,只是 work distribution 从固定的 launch-time schedule 变成硬件驱动。
diff --git a/zh/chapter_data_layout/index.md b/zh/chapter_data_layout/index.md
index 19757ac2..cca80406 100644
--- a/zh/chapter_data_layout/index.md
+++ b/zh/chapter_data_layout/index.md
@@ -1,92 +1,168 @@
(chap_data_layout)=
# 数据布局及其记号
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- *数据布局*把 tensor 的逻辑索引映射到物理位置,并决定 coalescing、bank conflict,以及某个硬件引擎是否能读这个 tile。
+- 本书用一套统一记号描述布局:`S[(shape) : (strides)]`,并配合命名轴(`@laneid`、`@TLane` 等)以及用于 broadcast 或复制数据的 replication term `R[...]`。
+- Swizzle 是一种基于 XOR 的地址重映射,用来消除 shared memory bank conflict。
:::
-TODO:翻译导言部分。
+同一组数值,如果以不同的物理排列写入内存,在同一块 GPU 上的运行速度可能相差一个数量级。
+
+原因在于,tensor 的逻辑索引本身并不说明它的 byte 实际放在哪里。硬件对这个位置非常敏感:它决定了 32 个 lane 的 load 是合并成一次 transaction,还是分散成 32 次;决定了这些地址是落在不同 memory bank 上,还是撞到同一个 bank 后被串行化;甚至还决定了一个 tile 是否符合 Tensor Core 能够读取的 byte 排列。
+
+机器学习程序通常用逻辑 shape 来描述 tensor。**数据布局**补上缺失的物理部分:它说明带有逻辑索引 `(i, j, …)` 的元素位于哪里,不论这个位置是在内存中、寄存器中,还是其他硬件存储中。
+
+本章介绍现代 GPU 编程中会遇到的主要布局。为了让讨论可控,我们会发展一套紧凑的**记号**,用它描述机器学习系统中不同场景下的布局。最后,我们会讨论 **swizzling**:一种让同一个 tile 的 row-wise 和 column-wise 访问都高效的机制。
## Shape-Stride 模型
-TODO:翻译 “The Shape-Stride Model” 小节。
+在进入 GPU-specific 的布局之前,值得先从最简单的布局开始,因为本章后面的内容都建立在它之上。布局的核心只有两件事:一个 **shape**,以及一组与之匹配的 **stride**。我们把这一对写作 `S[(shape) : (strides)]`。要找到某个逻辑索引的位置,只需要把这个索引与 strides 做点积。例如,一个 row-major 的 4×4 矩阵可以写成:
+
+```text
+S[(4, 4) : (4, 1)] addr(i, j) = i·4 + j·1
+```
+
+这只是经典 shape/stride 模型的紧凑写法(可以看作 CuTe 记号的 row-major 简化版),后面所有内容都从它扩展而来。
+
+事实上,你几乎肯定已经用过这个模型。任何写过 PyTorch 或 NumPy 的人都用过,因为这些库中的 tensor 本质上就是一个 shape,加上 flat storage buffer 上的一组 stride:
+
+```python
+import torch
+t = torch.arange(12).reshape(3, 4)
+t.shape # torch.Size([3, 4])
+t.stride() # (4, 1) ← exactly S[(3, 4) : (4, 1)]
+```
+
+一旦从这个角度看 tensor,很多 “reshape” 操作为什么完全不移动数据就很清楚了。它们只是重写 strides,并返回同一块 storage 上的一个 **view**。最清楚的例子是 transpose 或 permute:
+
+```python
+tt = t.permute(1, 0) # or t.T
+tt.shape # torch.Size([4, 3])
+tt.stride() # (1, 4) ← strides swapped, no data moved
+tt.data_ptr() == t.data_ptr() # True, same bytes
+```
+
+这里 `t.permute(1, 0)` 是同一块内存上的 `S[(4, 3) : (1, 4)]`:transpose 只是 stride 改变,没有移动任何 byte。对 contiguous tensor 上的 `reshape` 或 `view` 也是同样道理:旧 storage 上的新 shape 和新 stride。(NumPy 的行为也相同;唯一差别是它的 `.strides` 以 byte 而不是 element 为单位。)
+
+GPU 上的布局也是这样工作的。本章剩下的内容本质上是一系列围绕同一思想的变体:一个 tile 的映射,不论是映射到内存,还是通过稍后介绍的命名轴映射到 lane 和寄存器,都是固定 buffer 上的 stride 规则。因此,重排 tile 通常是改变 *layout*,而不是 copy。不过,我们也要小心这个推理的边界。零拷贝的说法只对单个线性地址空间上的逻辑 view 清晰成立;在 GPU 上,它只在新的 view 与已有 byte 排列和 ownership 安排兼容时成立。一旦改变哪个线程或寄存器拥有某个元素,或者改变 SMEM swizzle,通常就需要真正的数据搬运:load、store、shuffle、`ldmatrix`、transpose。
## Tile Layout
-TODO:翻译 “Tile Layout” 小节。
+到目前为止,我们描述的是整个 tensor 的布局。但 GPU kernel 很少一次性操作整张矩阵;它们会处理较小的 tile,这些 tile 会被加载、变换,并由不同硬件部分参与计算。好消息是,tiling 不需要新概念。它仍然只是布局,只不过多了几个维度。把一个 8×8 矩阵切成 2×4 tile,我们会得到一个 4-D 布局,坐标是 `(tile_row, row_in_tile, tile_col, col_in_tile)`,stride 的选择让每个 tile 保持连续:
+
+```text
+S[(4, 2, 2, 4) : (16, 4, 8, 1)]
+```
+
+逻辑 `(i, j)` 会先变成 `(i//2, j//4, i%2, j%4)`,再经过 strides 计算地址。值得注意的是,这套记号并没有引入任何特殊的 “tile” 概念,就表达了 tiling:它仍然是同一个 shape-stride 模型,只是把索引拆成了 outer 和 inner 坐标。
+
+下面的交互图展示了一个逻辑矩阵索引如何分解成 tile 坐标,再映射到物理地址。
```{raw} html
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
-
-*点击图中组件查看细节:TODO:翻译 tile layout 图注。*
+*交互图:点击一个 cell,查看它的 tiled index 和 address。*
## 命名轴
-TODO:翻译 “Named Axes” 小节。
+到目前为止,`S[...]` 中的每个 stride 都表示线性内存中的 offset,我们也把 address 当作这个空间中的位置。但在 GPU 上,数据可以位于不止一个地方:除了内存,一个 tile 还可能分布在 warp lane 上、线程寄存器上,或者 TMEM lane 与 column 上。为了统一描述这些情况,我们给记号扩展出**命名轴**。核心想法是让每个 stride 系数携带一个 axis tag,说明它穿过的是哪个空间:`@m` 表示普通内存,`@laneid` 表示 warp lane,`@reg` 表示寄存器,`@warpid` 表示 warp,`@TLane` / `@TCol` 表示 TMEM 坐标。有了这些 tag,单个布局不仅能描述数据在内存中的位置,也能描述它如何分布在实际操作这些数据的硬件资源上。
+
+把 memory tag 显式写出来之后,内存中的一个 row-major 8×16 tile 就是:
+
+```text
+S[(8, 16) : (16@m, 1@m)]
+```
+
+当布局描述的数据是*分布在线程之间*,而不是放在线性内存中时,这些 tag 就开始发挥作用。例如 `S[(8, 4, 2) : (4@laneid, 1@laneid, 1@reg)]` 并不指向线性内存,而是把行列映射到 lane ID 和每个 lane 的寄存器上。这里 `laneid` 表示 warp 内的 lane index,也就是 `thread_index % warp_size`。这正是你会在 {ref}`chap_layout_generations` 中遇到的 tensor-core register fragment。
+
+下面的交互图展示了一个布局如何把 tensor element 分布到 warp lane 和 per-lane register 上,而不是放进线性内存中。
```{raw} html
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
-
-*点击图中组件查看细节:TODO:翻译 named axes 图注。*
+*交互图:一个覆盖 `@laneid` 和 `@reg` 的布局;点击 cell 可以看到哪个 lane/register 持有它。*
## 分布式布局
-TODO:翻译 “Distributed Layout” 小节。
+命名轴最有用的地方在于,它们让我们可以统一描述系统中很多层级上的 placement,包括*跨设备*的 placement。刚才我们用它们描述单个 GPU 内部的 lane 和 register,但同样的思想可以继续向外延伸:`@gpuid_x` 和 `@gpuid_y` 这样的轴可以说明数据位于 GPU mesh 中的哪里。有了这些轴,记号就能捕捉分布式训练和推理中出现的 sharding pattern。不过,这些轴还没有描述*复制*,也就是被拷贝到多个位置的数据。因此我们加入 `R[n : stride]` 记号,其中 `R` 标记 replicated dimension。例如,`R[2 : 1@gpuid_x]` 表示沿 `@gpuid_x` 轴复制。把两者合在一起,单个表达式就能同时描述 tensor 在 2×2 GPU mesh 上的 sharding,以及沿一个轴的 replication:
+
+```text
+S[(2, 4, 8) : (1@gpuid_y, 8@m, 1@m)] + R[2 : 1@gpuid_x]
+```
+
+下面的 demo 展示了一个小 GPU mesh 上 partition-and-replication 的组合模式。点击任意 cell 可以看到哪个 device 持有它,并观察 `@gpuid_x` replication 如何把同一个副本放到配对 device 上;按钮可以在 fully-sharded、shard + replica 和 shard + offset 布局之间切换。
```{raw} html
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
-
-*点击图中组件查看细节:TODO:翻译 distributed layout 图注。*
+*交互图:分布在 2×2 GPU mesh 上的布局;点击 cell 可以看到哪些 device 持有它。*
### Kernel 内复制模式:TMEM 中的 Scale Factor
-TODO:翻译 “Intra-Kernel Replication Pattern: Scale Factors in TMEM” 小节。
+我们刚才为 GPU mesh 引入的 replication dimension `R[...]`,不只适用于多设备。事实证明,同一个结构也可以描述单个 kernel 内部发生的事情:硬件*跨 lane broadcast* 的数据。Blackwell 的 block-scaled MMA({ref}`chap_layout_generations`)就是很好的例子。它的 scale factor 位于 TMEM 中,其中一个 128-row scale vector 只存储在 **32 个 TMEM lane** 中,逻辑行 `r` 映射到 TMEM lane `r % 32`,而 `r // 32` 沿 column 方向前进。随后这 32 个存储的 TMEM lane 会沿 TMEM `TLane` 轴**复制**,从 32 个 lane 扩展到 128 个 lane,这样读取 warpgroup 中的四个 warp 都能在自己的 32-lane TMEM window 中找到一份副本。这是一个 `warpx4` broadcast,我们用 replication dimension 写出它。读取本身由这些 warp 的线程执行:
+
+```text
+S[(32, …) : (1@TLane, …)] + R[4 : 32@TLane]
+```
+
+这会在 32 个 TMEM lane 的 stride 上产生四份 replica:TMEM lane `l`、`l+32`、`l+64` 和 `l+96` 都保存同一个 scale。和前面一样,replication dimension 不携带新数据;它只是说明“同一个值位于四个 TMEM-lane 位置”,就像刚才 `@gpuid_x` 在 GPU mesh 上 broadcast 一行一样。
+
+下面的交互 demo 同时展示两步:先紧凑打包到 32 个 TMEM lane 中,再通过 `warpx4` broadcast 扩展到 128 个读取 lane。
```{raw} html
+ style="width:100%; min-width:1040px; height:560px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
+*交互图:点击一个 scale factor `SFA[m, sf]`;它会被打包到 TMEM lane `m mod 32`、column `(m // 32)·4 + sf`,随后沿 `TLane` 轴做 `warpx4` broadcast,产生四个 lane copy(`l`、`l+32`、`l+64`、`l+96`),每个 warp 的 32-lane window 各一份。*
+
+每个 column 内的 byte packing(`scale_vec` 的 1X/2X/4X 模式)以及 `cta_group::2` split 会在 {ref}`chap_layout_generations` 中介绍。
-*点击图中组件查看细节:TODO:翻译 scale factors in TMEM 图注。*
+已经熟悉 CuTe 的读者可以把本章记号理解为 CuTe 的 row-major 变体,并额外扩展了显式硬件命名轴以及专用 replication 结构。
## Swizzle Layout
-TODO:翻译 “Swizzle Layout” 小节。
+本章最后一种布局是为了解决一个具体硬件问题。GPU 上的 shared memory 被组织成多个 memory bank。当不同 lane 落在不同 bank 上时,访问最快。反过来,如果多个 lane 访问同一个 bank 内的不同地址,硬件只能把它们串行化,我们就要付出 **bank conflict** 的代价。
+
+在 tensor 程序中,这很难避免,因为内存访问并不总是纯线性顺序。处理矩阵时,我们经常需要从同一个 tile 中读取 row slice 和 column slice,这会产生真实的张力:对 row-wise 访问高效的布局,通常会让 column-wise 访问产生 bank conflict;而偏向 column 的布局又会伤害 row 访问。**Swizzling** 就是为了解开这个张力而设计的技术。
+
+Swizzle 的想法是重排地址映射,通常是把 column index 与 row 做 XOR,使得 *row* 和 *column* 两类访问最终都分散到不同 bank 上。它提供的 conflict-free 保证是有条件的:只对匹配的 element width、swizzle mode 和 access pattern 成立,也就是对应 engine descriptor 所期望的模式;它并不保证任意 element width 或 alignment 都无冲突。
+
+第一个交互 demo 让这个过程具体化。点击一个 column index,观察每个元素落到哪个 bank:左侧普通 row-major tile 中,一个 column 会把 8 个元素全部汇入同一个 bank,因此读取会串行化成 8 个 cycle;右侧 XOR-swizzled layout 中,同一个 column 被分散到 8 个不同 bank,可以在 1 个 cycle 中读完。
```{raw} html
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
+*交互图:一个 8×8 tile,普通 row-major 按列读取会产生 bank conflict,XOR swizzle 后变成 conflict-free。*
+
+这个小的 8×8 例子抓住了核心思想,但真实 GPU 内存的 bank 数远多于这个玩具图。为了让 swizzling 在完整尺度上工作,我们不会把整个 tile 当作一个单块对象处理。相反,我们会把内存切成小 segment,并在每个 segment 内应用 swizzle pattern。实践中最常见的是 `SWIZZLE_128B`,围绕 128-byte segment 组织,使同样的 row/column 重映射技巧自然适配 32-bank memory system。
-*点击图中组件查看细节:TODO:翻译 8x8 XOR swizzle 图注。*
+下面的交互 demo 展示一个具体硬件 swizzle:`SWIZZLE_128B`。在我们推广到其他格式之前,可以先看到这个逐 segment 重复的 pattern。
```{raw} html
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
+*交互图:128-byte segment 内的 `SWIZZLE_128B` pattern;逐 cycle 查看读取过程,可以看到 `physical_sector = logical_sector XOR row` 如何把每个 column 分散到不同 bank。*
+
+同一个思想也可以扩展到 128-byte 之外。为了简化可视化,我们接下来会用单个彩色 block 表示一个 segment,而不是画出每个 bank。一般来说,硬件会定义一个小的重复 **atom**,在 atom 内应用 permutation;不同 swizzle mode 选择不同的 atom 大小。`SWIZZLE_128B` 使用 8 × 128 B atom,`SWIZZLE_64B` 使用 8 × 64 B atom,`SWIZZLE_32B` 使用 8 × 32 B atom;整个 tile 再由所选 atom 平铺而成。
-*点击图中组件查看细节:TODO:翻译 SWIZZLE_128B 图注。*
+最后一个交互 demo 可以在这些格式之间切换(也包括一种 16 B interleaved mode),选择数据类型,并把鼠标悬停在任意 cell 上直接查看一个 atom 内部的 element 排列。对于判断某条 load/store 指令期望哪种 swizzle,这正是合适的细节层级。
```{raw} html
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
```
+*交互图:选择 swizzle format 和数据类型,查看它的 atom shape(8 × N B);悬停在 cell 上可以看到其中的元素如何被 permutation。*
+
+应该选择哪种 mode?经验法则是优先选择 tile 能填满的*最大* atom。一个 N-byte atom 要求 tile 的 contiguous dimension 至少有 N byte,并且是 N 的倍数,因此 `SWIZZLE_128B` 只有在一行至少跨越 128 byte,也就是 64 个 `float16` 元素时才适用。只要能适用,它就是默认选择,因为它的 8 × 128 B atom 覆盖一整条 128-byte bank line,可以一次把一个 column 分散到全部 32 个 bank,在 fp16 中同时给 8 行和 8 列提供 conflict-free 访问。但如果问题 shape 迫使 contiguous dimension 变小,tile 无法填满 128 B atom,就要降到 `SWIZZLE_64B` 或 `SWIZZLE_32B`,选择 row 仍然可以覆盖的最大 atom。
+
+你不需要手工推导这些 permutation address。这里值得精确说明 swizzle 与 `S[...]` 记号的关系:swizzle *不是* affine map 的一部分。它是组合在 affine map 之上的一个单独的非仿射层。`S[...]` 布局把元素放到线性内存(`@m`)地址上,swizzle 再重排这个地址。在 TIRx layout API 中,这写作 `ComposeLayout(swizzle, tile)`({ref}`chap_tirx_layout_api`)。你的工作只是为所有会触碰这个 tile 的 op 选择一致的 mode,然后让组合布局处理剩下的事。
-*点击图中组件查看细节:TODO:翻译 swizzle atom 图注。*
+硬件填充的也是同一个组合布局,这正是 swizzling 与 tiling 汇合的地方。TMA descriptor 是多维的,因此一个三维 box 可以同时描述 tile 的 atom tiling 以及每个 atom 内部的 swizzle;一次 TMA load 随后会 atom by atom 地布置 tile,并在写 shared memory 时应用 swizzle({ref}`chap_tma`),不需要单独的 swizzling pass。每个 engine 需要*哪一种* swizzle 是 generation-specific 的,这就是下一章的主题。
diff --git a/zh/chapter_flash_attention/index.md b/zh/chapter_flash_attention/index.md
index f00ed0b0..73ab1880 100644
--- a/zh/chapter_flash_attention/index.md
+++ b/zh/chapter_flash_attention/index.md
@@ -1,10 +1,607 @@
----
-orphan: true
----
-
(chap_flash_attention)=
# Flash Attention 4
-> 翻译状态:待翻译。对应英文章节:`chapter_flash_attention/index.md`。
+:::{admonition} 概览
+:class: overview
+
+- Attention 会运行两个 MMA,中间夹着 softmax,因此它不能像 GEMM 那样只是重复一个 MMA。
+- Kernel 会把第一部分的硬件 primitive(TMA、`tcgen05`、TMEM、barrier)和第三部分的 GEMM 技术组合起来,再加入 warp role、online-softmax rescaling、causal masking 和 GQA。
+:::
+
+Attention 是决定 transformer 能否运行的 kernel,也是前面构建的所有东西最终必须协同工作的地方。我们为 GEMM 组装的每个部件都会带到这里:TMA tile movement、`tcgen05` MMA、TMEM、warpgroup register tile,以及显式 barrier。
+
+挑战在于 attention 不是一个 MMA 的重复。它是两个 MMA,中间夹着真正的工作:online softmax、causal masking,以及让早期 block 和后续 block 保持同一尺度的 rescaling。
+
+新的难点就在中间这一段。普通 matmul 只会加到 accumulator;attention 必须在新的 key 和 value stream in 时,重新访问并 rescale 已经计算过的结果。Softmax 本身也在两个 Tensor Core MMA 之间运行在 CUDA core 上,因此 exponential 和 row-wise reduction 直接位于关键路径上。
+
+这就是为什么 attention 优化很大程度上就是 softmax 优化:重写 `exp`,并让 softmax 与 MMA 重叠,而不是在 softmax 上停住。
+
+本章目标不是从零重新推导 Flash Attention。我们会保留足够算法背景,让 kernel 可读,然后把注意力放在真正的新内容上:这个算法如何变成 TIRx。
+
+最清晰的入口是跟随一个 tile 在 kernel 中流动。`Q`、`K` 和 `V` 作为 input tile 进入,从 GMEM 加载到 SMEM。Score MMA 把 `Q` 和 `K` 相乘,得到 TMEM 中的 score tile `S`。Softmax 把 `S` 变成 numerator tile `P`,value MMA 再把 `P` 和 `V` 组合起来更新 output accumulator `O`。
+
+到目前为止,这看起来像两个 matmul 粘在一起,但这里有一个 GEMM 从不需要处理的 twist:每当 running softmax maximum 改变,已经累加的 `O` 就突然处在错误尺度上。它必须先被 rescale,下一次 value MMA 才能安全地加到其中。下面各节会先追踪这条路径,然后展示 TIRx 如何把每个 stage 交给 warpgroup,并把 stage 串接起来。
+
+## 算法形状
+
+在把 tile 放进内存之前,我们需要知道这些 tile 服务的算法。对于一个 query block,Flash Attention 计算:
+
+$$O = \text{softmax}(QK^{\top} / \sqrt{d})V$$
+
+字面看,公式说要构造完整 score matrix `S = QKᵀ`,对它做 softmax,再乘以 `V`。这正是我们不能使用的方法,因为完整 `S` 太大。seq=4096 时,每个 head 大约有 16M 个元素,fp32 下约 64 MB,比 SMEM 或单个 128×512 TMEM region 大几个数量级。片上根本没有地方放它。Flash Attention 的答案是永远不 materialize `S`。相反,它按 block stream `K/V`,并携带三个 per-row running state 来总结目前为止看到的全部内容:
+
+- `row_max`:目前见过的最大 score。
+- `row_sum`:softmax 的 running denominator。
+- `O`:running output accumulator。
+
+Streaming update 会在新 block 到达时保持这些 state 正确。微妙之处在于,每处理一个 block,running max 都可能上升;一旦上升,在旧 max 下计算的一切都处在错误尺度上。因此在加入新贡献之前,我们必须先把旧状态拉回新尺度:
+
+```text
+S = Q_block @ K_block.T
+m_new = max(row_max, rowmax(S))
+scale = exp((row_max - m_new) / sqrt(d))
+P = exp((S - m_new) / sqrt(d))
+row_sum = row_sum * scale + rowsum(P)
+O = O * scale + P @ V_block
+row_max = m_new
+```
+
+单个 `scale` 因子在这里有双重作用:它同时 rescale running denominator 和 running output,使早期 block 和后续 block 的贡献最终位于同一尺度。
+
+上面的 pseudocode 使用自然 `exp` 和显式 `/sqrt(d)`,因为这样最容易读,但 kernel 采用更便宜的路径。它把 `1/sqrt(d)` 和 `log2(e)` 合成一个常量 `scale_log2 = log2(e)/sqrt(d)`,并用硬件 `exp2` 对 raw score 计算所有 exponential,使用恒等式 `exp(x/sqrt(d)) = exp2(x · scale_log2)`。原因很简单:在这个硬件上,`exp2` 比自然 `exp` 更快。
+
+继续之前,有一点要明确:这里的 `P` *不是*最终 normalized attention matrix。它只是当前 K/V block 的 softmax numerator。Normalization 被故意推迟,只有最后一个 block 之后,kernel 才写出 `O / row_sum`。
+
+对于 TIRx,知道算法计算什么只是一半。另一半是 kernel 运行时*每个 tile 位于哪里*,因为这决定 layout 和 barrier 代码。`S`、`P` 和 `O` 都是 tile value,并且各自有家:
+
+- `S` 是 score tile。Score MMA 把它写入 TMEM。
+- `P` 是 softmax numerator tile。Softmax 从 TMEM 把 `S` 读入寄存器,计算 `P = exp((S - m_new) / sqrt(d))`,再把 `P` 写回 TMEM。
+- `O` 是 output accumulator tile。Value MMA 从 TMEM 读取 `P`,从 SMEM 读取 `V`,然后累加到 TMEM 中的 `O`。
+
+前面提到的 rescale 也是一个 tile operation,不是 scalar bookkeeping:当 `row_max` 改变时,旧 `O` 会从 TMEM 读出,在寄存器中乘上 scale,再写回 TMEM,然后下一次 value MMA 才能累加进去。后面每节都会沿着同一结构展开:tile placement、hardware path,以及证明下一个 consumer 可以运行的 barrier。
+
+## Tile-Primitive Graph
+
+有了 running state 和它们的位置,我们可以把算法展开成具体的 tile movement 序列。对于一个 K/V block,kernel 从上到下走这条 tile 路径:
+
+```text
+Q, K, V in GMEM
+ -> Q, K, V in SMEM by TMA load
+ -> S in TMEM by score MMA: QK^T
+ -> P in TMEM by softmax numerator: TMEM -> RF -> TMEM
+ -> O in TMEM by value MMA: P V
+ -> O in GMEM by normalization, SMEM staging, and TMA store
+```
+
+与 GEMM 的差异归结为一行。GEMM 是重复的一条 MMA chain;FA4 有两个 MMA phase,中间夹着 softmax。后面几乎所有内容都是这个额外 stage 的后果。
+
+如果把短路径展开成显式 producer-consumer edge,就得到完整 graph:
+
+| Stage | Tile movement or compute | TIRx primitive | Hardware path |
+|-------|--------------------------|----------------|---------------|
+| Load Q/K/V | GMEM tiles -> SMEM tiles | `Tx.copy_async(..., dispatch="tma")` | TMA load |
+| Score MMA | SMEM 中的 Q 和 SMEM 中的 K -> TMEM 中的 score tile `S` | `Tx.warp.gemm_async(..., dispatch="tcgen05")` | `tcgen05.mma` |
+| Softmax read | TMEM 中的 `S` -> warpgroup register tile | `Tx.wg.copy_async(reg, tmem)` | `tcgen05.ld` |
+| Softmax write | registers 中的 numerator tile `P` -> fp16 TMEM view | `Tx.copy_async(tmem_as_f16, reg)` | TMEM store,随后 `tcgen05.wait.st()` |
+| Value MMA | TMEM 中的 `P` 和 SMEM 中的 V -> TMEM 中的 output accumulator `O` | `Tx.warp.gemm_async(..., dispatch="tcgen05")` | 带 TMEM operand 的 `tcgen05.mma` |
+| Correction | TMEM 中的 `O` -> registers -> TMEM 中的 `O` | TMEM readback、register multiply、TMEM store | `tcgen05.ld` / TMEM store |
+| Epilogue | 最终 TMEM 中的 `O` -> registers -> SMEM -> GMEM | TMEM readback、`Tx.copy`、TMA store | `tcgen05.ld` + TMA store |
+
+新增行是 softmax 和 correction。二者都会加入 TMEM -> register -> TMEM 流量,也会在 score MMA 和 value MMA 之间创建额外交接。
+
+**Try with your agent**:让它只 trace 上面的短路径。对每个箭头,命名 producer stage、consumer stage、source tile、destination tile 和 hardware path。然后问哪些箭头在 GEMM 章节中不存在。
+
+## Warp 角色和 Scope
+
+数据路径确定后,自然的问题是谁实际运行每个 stage。这里每个 CTA 有 4 个 warpgroup,总共 512 个线程,它们不是按触碰的数据来切分,而是按 warpgroup 执行的*工作类型*切分:
+
+- WG3 驱动硬件引擎:TMA load、MMA 和 TMA store。
+- WG0、WG1 和 WG2 执行这些 engine 调用之间发生的 register-heavy math:softmax、correction 和 epilogue。
+
+精确角色表是:
+
+| Owner | Role | What it does |
+|-------|------|--------------|
+| WG3, warp 1 | TMA load | 从 GMEM 加载 Q、K、V tile 到 SMEM |
+| WG3, warp 0 | MMA | 发起 score MMA 和 value MMA |
+| WG3, warp 2 | TMA store | 把最终 O tile 从 SMEM store 到 GMEM |
+| WG0 | Q stage 0 的 softmax | 读取 TMEM 中的 S,计算 P,把 P 写回 TMEM |
+| WG1 | Q stage 1 的 softmax | 为第二个 Q pipeline stage 执行同样工作 |
+| WG2 | Correction 和 epilogue | Rescale TMEM 中的 O,normalize,并 stage 输出 |
+
+很容易把 “两个 Q stage” 误读成两个 attention head,但它们不是。它们只是 Q pipeline 中的两个 slot,由 WG0 拥有一个、WG1 拥有另一个,因此两个 Q tile 可以同时在飞行中。这就是 softmax 工作出现两次的原因:WG0 一次,WG1 一次。
+
+代码用符号坐标选择这些角色:
+
+```python
+wg_id = T.warpgroup_id([4])
+warp_id = T.warp_id_in_wg([4])
+```
+
+阅读 kernel 时,先找到 role branch。它会告诉你嵌套在其中的每个 tile primitive 由哪一队线程拥有。
+
+- WG3 warp 1 启动 TMA load 命令。一个 elected lane 发起 copy,TMA engine 移动 tile。
+- WG3 warp 0 发起 `tcgen05.mma` 指令。
+- WG0 和 WG1 以完整 warpgroup scope 运行 softmax。
+- WG2 以完整 warpgroup scope 运行 correction 和 epilogue。
+
+有一个不对称性会塑造整个 barrier graph:*所有* MMA,无论 score 还是 value,都只由 WG3 warp 0 发起。WG0 和 WG1 完全不发起 MMA。它们只消费 score tile,运行 softmax,并把 `P` 写回 TMEM。
+
+正因为这种分离,softmax 周围需要 barrier。`s_ready` 把 score tile 从 MMA warp 传给 softmax;`p_o_rescale` 传递 `P`,以及一个对 value MMA 安全的 `O` slot:要么已经 rescale,要么因为不需要 rescale 而被 release。本章后面会反复回到这两个名字。
+
+## 读取代码片段
+
+本章的代码片段摘自 [`flash_attention4.py`](https://github.com/mlc-ai/tirx-kernels/blob/main/tirx_kernels/attention/flash_attention4.py),因此不可避免会引用一些没有在本章完整复现的 kernel 名称。自解释的名字(`wg_id`、`warp_id`、`BLK_M`/`BLK_N`、`HEAD_DIM`、`kv_stage`、`SMEM_PIPE_DEPTH_*` / `TMEM_PIPE_DEPTH` 深度、`should_accumulate`,以及这里为 1 的 `CTA_GROUP`)会在第一次相关时引入。其他名字在下表给出一行说明,这样当片段出现陌生名字时可以立即查到:
+
+| Name | Meaning |
+|------|---------|
+| `q_stage`, `i_q` | Q pipeline stage,0 或 1,即哪个 Q tile slot(`SMEM_PIPE_DEPTH_Q = 2`)。在 WG0/WG1 softmax 内部,warpgroup 自己的 `wg_id`(0 或 1)就是这个 stage index,因此 `S_region[q_stage]`、`P_region[wg_id]` 和 `O_region[i_q]` 都选择同一个 Q stage |
+| `MMA_N` | score/output tile 在 TMEM column 上的宽度(128) |
+| `MMA_K` | `P`/`V` column 上的 MMA inner-K step(16);`K_SPLIT = 6 * MMA_K = 96` |
+| `K_SPLIT` | value-MMA schedule 的 split point(见 *The Two MMA Phases*);第一次 value MMA 覆盖 columns `0:K_SPLIT`(`6 * MMA_K = 96`) |
+| `should_rescale` | WG2 per-row flag:旧 `O` 是否需要在下一次 value MMA 前 rescale(通过 `any_sync` 跨 warpgroup reduce) |
+| `rescale_threshold` | 小 row-max 变化的 skip threshold;当前 kernel 使用 `8.0`,被跳过的 rescale 会把 `acc_scale` 精确设为 `1.0` |
+| `scale_log2` | log2 单位下的 softmax scale,`log2(e)/√d`,因此 `P = exp2((S - m) · scale_log2)` |
+| `acc_scale` | softmax 通过 SMEM mailbox 传给 WG2 的 per-row rescale factor |
+| `chunk_start`/`chunk_end`, `p_start`/`p_end` | 正在读/写的 32-wide softmax chunk 的 column range |
+
+## 两个 MMA Phase
+
+对于每个 streamed K/V tile,Flash Attention 会运行两个 MMA phase,并由 softmax 连接:
+
+```text
+Q, K -> score MMA -> S
+S -> softmax -> P
+P, V -> value MMA -> O
+```
+
+可以把它看成三个连续 producer 的 pipeline。第一个 MMA 产生 attention score `S`,softmax 把 `S` 变成 numerator `P`,第二个 MMA 消费 `P` 来更新 output accumulator `O`。对 `row_sum` 的 normalization 会推迟到 epilogue,等每个 K/V tile 都贡献之后再做。
+
+下面每个 tile op 都会得到和 GEMM step 一样的 **scope / layout / dispatch** 卡片,并额外加入 **Handoff**,命名把 tile 交给下一角色的 barrier。
+
+Compute 代码从不直接说 raw TMEM column number。Kernel 会把单个 TMEM allocation 切成 per-stage view(`S_region`、`P_region`、`O_region`),并用 pipeline stage 索引它们(`S_region[q_stage]`、`O_region[i_q]`、`P_region[i_q, 0:K_SPLIT]`)。这些 view 在 [TMEM Layout and Reuse](#tmem-layout-and-reuse) 一节中用 `T.TMEMStages` 定义;目前把每个 region 当作同一块物理 TMEM 的命名 slice 即可。
+
+### Score MMA
+
+两个 phase 中的第一个是 score MMA,也就是每个 K/V iteration 开头的 matmul。它计算:
+
+$$S = Q_{\text{block}}K_{\text{block}}^{\top}$$
+
+并把 `128 x 128` score tile 写入 TMEM:
+
+```python
+Tx.warp.gemm_async(
+ S_region[q_stage],
+ Q_smem[q_stage, 0:BLK_M, 0:HEAD_DIM],
+ K_smem[kv_stage, 0:BLK_N, 0:HEAD_DIM],
+ dispatch="tcgen05",
+ cta_group=CTA_GROUP,
+)
+if T.ptx.elect_sync():
+ s_ready.arrive(q_stage)
+```
+
+我们可以问 GEMM 章节对每个 tile op 都问过的四个问题:谁运行它、tile 位于哪里、如何 dispatch、如何 handoff:
+
+> **Tile-primitive readout:Score MMA**
+> - Scope:WG3 warp 0 发起;一个 elected lane arrive `s_ready`。
+> - Layout:SMEM 中的 Q、K -> TMEM 中的 `S`(`S_region[q_stage]`)。
+> - Dispatch:`tcgen05`。
+> - Handoff:`s_ready`(-> softmax)。
+
+单个 elected thread 在 `s_ready` 上 arrive,就是整个 handoff。它宣布这个 score tile 已经完成,softmax warpgroup 现在可以读取它。
+
+### 两个 MMA 之间的 Softmax
+
+两个 MMA 中间是 softmax,它把 score tile `S` 变成 numerator tile `P`。它的 readout card 是:
+
+> **Tile-primitive readout:Softmax**
+> - Scope:WG0(Q stage 0)/ WG1(Q stage 1),完整 warpgroup。
+> - Layout:TMEM 中的 `S` -> registers -> fp16 TMEM 中的 `P`(`P_region[wg_id]`)。
+> - Dispatch:用 `tcgen05.ld` 读取,用 TMEM store 写入;中间在 registers 中做 row-wise math。
+> - Handoff:等待 `s_ready`;arrive `p_o_rescale`(前 96 列)和 `p_ready_2`(最后 32 列)。
+
+这个 stage 完全没有 GEMM 对应物。WG0/WG1 等待 score tile 在 `s_ready` 上到达,然后按 register-sized chunk 从 TMEM 读出:
+
+```python
+Tx.copy_async(
+ s_chunk[:, chunk_start : chunk_end],
+ S_region[wg_id, chunk_start : chunk_end],
+)
+```
+
+这是一个 warpgroup scope 下的 TMEM-to-register tile read。Scores 进入寄存器后,softmax warpgroup 按顺序做三件事:
+
+1. 计算 row max 和 row sum,
+2. 计算 softmax numerator tile `P`,
+3. 把 `P` 作为 fp16 写回 TMEM。
+
+最后一步如下:
+
+```python
+Tx.copy_async(
+ P_region[wg_id, p_start : p_end],
+ p_chunk[:, p_start : p_end],
+)
+```
+
+既然刚刚在寄存器中算出了 `P`,为什么还要写回 TMEM?因为 value MMA 需要 `P` 作为一个 *tile operand*,而 MMA 不能把散落在每个线程中的 scalar register 当成矩阵读取。在这个 kernel 中,`P` 的 MMA-readable 形式是 `P_region`,也就是 fp16 TMEM alias `tmem_as_f16` 上的一个 view。因此这个 writeback 不是多余移动,而是把 `P` 放到下一次 MMA 真正能消费的唯一形状中。
+
+### Value MMA
+
+第二个 phase,也是每个 K/V iteration 收尾的 phase,是 value MMA。它计算:
+
+$$O = O + P_{\text{block}}V_{\text{block}}$$
+
+当这个 MMA 运行时,`O` 已经处在当前 K/V block 所需的正确状态:第一个 block 上被初始化,后续 block 上被 rescale。因此 MMA 只需要累加。它与 GEMM 的区别在于 operand 位于哪里:A operand 是 TMEM 中的 `P`,B operand 是 SMEM 中的 `V`,accumulator `O` 也在 TMEM 中:
+
+```python
+# First sub-MMA: columns 0:K_SPLIT (the first 96 of P / rows of V).
+Tx.warp.gemm_async(
+ O_region[i_q],
+ P_region[i_q, 0:K_SPLIT],
+ V_smem[kv_stage, 0:K_SPLIT, 0:HEAD_DIM],
+ transB=True,
+ accum=should_accumulate,
+ dispatch="tcgen05",
+ cta_group=CTA_GROUP,
+)
+# The second sub-MMA (same form, accum=True, gated on p_ready_2) covers the
+# remaining columns K_SPLIT:BLK_N.
+```
+
+> **Tile-primitive readout:Value MMA**
+> - Scope:WG3 warp 0。
+> - Layout:TMEM 中的 `P` + SMEM 中的 V -> TMEM 中的 `O`(`O_region[i_q]`)。
+> - Dispatch:带 TMEM operand 的 `tcgen05`。
+> - Handoff:等待 `p_o_rescale`、`p_ready_2`、`kv_load.full`;arrive `o_ready`(-> epilogue)。
+
+这个 operand placement 是两个 MMA 之间的硬件差异:
+
+- Score MMA 从 SMEM 读取两个 operand:Q 和 K。
+- Value MMA 从 TMEM 读取一个 operand:`P`。
+- Value MMA 从 SMEM 读取另一个 operand:V。
+- 结果累加到 TMEM 中的 `O`。
+
+`accum=should_accumulate` flag 实现了算法中的“初始化或累加”选择:它在 query block 的第一个 K/V tile 上为 false,在后续每个 tile 上为 true。
+
+你还会注意到,value MMA 不是一次性运行,而是拆成 `96 + 32` schedule:
+
+1. Softmax 以四个 32-column chunk 写入 `P`。
+2. 前三个 chunk 就绪后,value MMA 立即开始处理 `P` 的前 96 列以及 V 中匹配的行。
+3. 最后 32 列等待 `p_ready_2`。
+4. 第二次 MMA 消费最后这个 chunk,并完成 tile。
+
+拆分的原因是让 Tensor Core 保持忙碌。如果把 value MMA 作为一次单独指令运行,整个 phase 都会停到所有四个 32-column `P` chunk 都 exponentiate 并 store 完成。先对前三个 chunk firing,kernel 就能把最后一个 chunk 的 `exp` 和 TMEM write 与一个已经在飞行中的 96-wide MMA 重叠起来,把原本的空闲时间变成有用工作。
+
+## TMEM Layout and Reuse
+
+`S`、`P` 和 `O` 都必须共享同一个 `128 x 512` TMEM allocation,而它们如何打包进这块空间,正是 barrier 和 layout 在这个 kernel 中不可分割的原因。
+
+下图直接展示这种 packing:score slot、numerator slot 和 output slot 全都共享同一个 TMEM allocation,因此 barrier protocol 才能让复用合法。
+
+
+
+这张图可以读作一组 tile slot:
+
+- Score slot 保存 `S = QK^T`。
+- Numerator slot 保存 softmax exponentiation 之后的 `P` tile。
+- Output slot 保存 fp32 `O` accumulator。
+
+这些不是独立 buffer。它们是*同一个* allocation 的区域,而这种共享不是风格选择,而是被迫的。Q-pipeline depth 为 2 时,两个 `S` slot(2 × MMA_N = 256 列)和两个 `O` slot(2 × MMA_N = 256 列)已经占满全部 512 个 fp32 列。没有剩余空间给 `P`,所以 `P` 只能通过更窄的 fp16 view alias 同一批 byte。这样安全的唯一原因是每个 region 都严格在前一个 consumer 完成后才复用,而这个时机正是 barrier 保证的。因此在 FA4 中,barrier 不只是 scheduling;它们首先让 layout 合法。
+
+Aliasing 通过 `T.TMEMPool` 设置。Kernel 先取一个 fp32 view(`tmem`)用于 score 和 output accumulator,然后把 pool base rewind 回 0,再在*同一批*物理 byte 上取第二个 fp16 view(`tmem_as_f16`):
+
+```python
+tmem_pool = T.TMEMPool(pool, total_cols=N_COLS_TMEM, cta_group=CTA_GROUP, tmem_addr=tmem_addr)
+tmem = tmem_pool.alloc((128, N_COLS_TMEM), "float32")
+tmem_pool.move_base_to(0)
+tmem_as_f16 = tmem_pool.alloc((128, N_COLS_TMEM * 2), "float16")
+tmem_pool.commit()
+```
+
+因为 fp16 元素宽度只有一半,fp16 view 会在同样 byte 上暴露出两倍数量的可索引 column,这正是 `P` 所处的空间,也是 fp32 layout 没有空间容纳的区域。有了两个 view 后,kernel 用 `T.TMEMStages` 把 `S`、`P` 和 `O` slot 切成 staged region,使 compute 代码可以按 pipeline stage 索引,而不是按 raw column:
+
+```python
+S_region = T.TMEMStages(tmem, col_start=0, width=MMA_N, stages=SMEM_PIPE_DEPTH_Q, stride=MMA_N)
+O_region = T.TMEMStages(tmem, col_start=MMA_N * SMEM_PIPE_DEPTH_Q, width=MMA_N, stages=SMEM_PIPE_DEPTH_Q, stride=MMA_N)
+P_region = T.TMEMStages(tmem_as_f16, col_start=MMA_N, width=BLK_N, stages=SMEM_PIPE_DEPTH_Q, stride=MMA_N * 2)
+```
+
+`P_region` stride 中的 `* 2` 是 aliasing 在代码中可见的地方。`S_region` 和 `O_region` 以 fp32 `tmem` column 计量,而 `P_region` 以 fp16 `tmem_as_f16` column 计量,后者宽度只有一半,因此 stage-to-stage movement 需要 doubled stride 才能落到同一批物理 byte 上。不过一旦 region 定义好,compute 代码就很干净:它写 `S_region[q_stage]`、读 `S_region[wg_id, ...]`、写 `P_region[wg_id, ...]`,并累加到 `O_region[i_q]`,完全不触碰 raw column index。
+
+**Try with your agent**:让它解释这个 FA4 kernel 中的 fp32(`tmem`)和 fp16(`tmem_as_f16`)view。哪些物理 TMEM region 保存 `S`、`P` 和 `O`?为什么 `P_region` 的 stride 使用 `MMA_N * 2`?复用问题留到下一节:看完 barrier table 后,再检查每个 region 复用前必须等哪些 consumer 完成。
+
+## Barrier 如何连接角色
+
+这是 kernel 最难的部分,所以最好循序渐进。先从沿主 compute path 移动数据的几组 barrier 开始,把其他 barrier 当作稍后可查的 bookkeeping。Data-ready handoff 是:
+
+| Handoff | Meaning |
+|---------|---------|
+| TMA load -> score/value MMA | Q、K 或 V 已经到达 SMEM,可以喂给 MMA |
+| score MMA -> softmax | `S` 已经在 TMEM 中 ready |
+| softmax/correction -> value MMA | `P` 已经在 TMEM 中 ready,且 `O` 可以安全 accumulation |
+| value MMA -> epilogue | 最终 `O` 已经在 TMEM 中 ready |
+| epilogue -> TMA store | `O_smem` 已经 ready,可以 store |
+
+不在这个列表里的都是 pipeline bookkeeping:release 某个 SMEM、TMEM 或 staging buffer,让另一个角色可以复用它。有用之处在于,每个 barrier 无论传递数据还是只做 bookkeeping,都可以按同一种 tile handoff 来读。你只要问谁生产数据、谁消费数据,以及双方完成后哪个 buffer 变得空闲。
+
+下一张图把这些 handoff 压缩成两个 MMA phase 的精确 readiness gate:score MMA 等待什么,value MMA 累加前必须等待什么。
+
+
+
+把这张图看成 correctness gate,而不是 schedule。它回答“这个 MMA firing 前必须满足什么”,不说明 timing。Score MMA 等待 Q 和 K 在 SMEM 中 ready,然后产生 `S`。Value MMA 同时等待三件事:SMEM 中的 V、softmax 产生的 `P` tile,以及 WG2 已经 release 或 rescale 过的 `O` slot。Softmax-to-value gate 被拆分,原因前面已经见过:当 `P` 的前 96 列就位时 value MMA 可以开始,`p_ready_2` 释放最后 32 列。
+
+有一个 handoff 不符合 tile-readiness 模型:softmax-to-correction edge。Softmax 不是传递 tile,而是通过一个 one-slot SMEM mailbox 向 WG2 传递单个 scalar(K/V loop 中是 `acc_scale`,epilogue 中是最终 `row_sum`)。由于这个 slot 每次 iteration 都会复用,一对 `full`/`empty` barrier 必须保护它。
+
+下图放大了 mailbox handshake,因此这对 barrier 应该读作 scalar producer-consumer channel,而不是 tile-ready gate。
+
+
+
+把 `softmax_corr.full` 和 `softmax_corr.empty` 读作一对 producer-consumer:
+
+1. Softmax 在复用 scale/sum slot 前等待 `softmax_corr.empty`。
+2. Softmax 把 `acc_scale` 或最终 `row_sum` 写入这个 slot。
+3. Softmax arrive `softmax_corr.full`。
+4. WG2 等待 `softmax_corr.full`,然后读取这个 slot。
+5. WG2 arrive `softmax_corr.empty`。
+6. Softmax warpgroup 可以在下一阶段复用这个 slot。
+
+要小心 `softmax_corr.empty` 的含义和不含义。它只表示 WG2 已经消费 scale/sum slot。它不表示 `P` 已经 ready,更*不是*允许 value MMA 启动的 gate。那个 gate 是 `p_o_rescale`,它在 `P` 的前 96 列写入、且 `O` slot 可以安全累加时触发。混淆二者是经典 wrong-result bug 来源。
+
+有了主路径之后,完整 barrier list 可以作为参考:
+
+| Barrier | Producer -> consumer | What becomes safe |
+|---------|----------------------|-------------------|
+| `q_load.full` | TMA load -> score MMA | Q SMEM tile 可以喂给 MMA |
+| `q_load.empty` | 这个 Q stage 的所有 score MMA -> TMA load | Q SMEM stage 可以为下一个 task 复用 |
+| `kv_load.full` | TMA load -> score/value MMA | K 或 V SMEM tile 可以喂给 MMA |
+| `kv_load.empty` | score/value MMA -> TMA load | K/V SMEM stage 可以复用 |
+| `s_ready` | score MMA -> softmax | S TMEM tile 可以读取 |
+| `p_o_rescale` | softmax + WG2 -> value MMA | P 的前 96 列在 TMEM 中,且 O slot 可用于 value MMA |
+| `p_ready_2` | softmax -> value MMA | P 的最后四分之一在 TMEM 中 |
+| `o_ready` | value MMA -> epilogue | 最终 O accumulator ready |
+| `softmax_corr.full` | softmax -> WG2 | `acc_scale` 或最终 `row_sum` 在 SMEM mailbox 中 ready |
+| `softmax_corr.empty` | WG2 -> softmax | WG2 读取后,同一个 SMEM mailbox slot 可以复用 |
+| `corr_epi.full` | epilogue -> TMA store | O_smem ready,可以 store |
+| `corr_epi.empty` | TMA store -> epilogue | O_smem stage 可以复用 |
+
+和 GEMM 一样,可以从 signal producer 推断 barrier type:
+
+- TMA load 使用 `TMABar`,因为 TMA engine 会 byte-count 自己的完成。
+- MMA completion 使用 `TCGen05Bar`,因为 `tcgen05.commit` signal completion group。
+- 纯 thread-to-thread handoff 使用 `MBarrier`,参与线程显式 arrive。
+
+拆分的 softmax-to-value handoff 值得再仔细看。它使用两个 gate:
+
+- `p_o_rescale` 在 `P` 的前 96 列写入且 `O` tile 可以安全累加时,允许 value MMA 开始。
+- `p_ready_2` 释放 `P` 的最后 32 列,匹配上一节的 `96 + 32` value-MMA schedule。
+
+第一个 K/V block 是简单情况。WG2 会 pre-arrive `p_o_rescale`,因为还没有旧的 `O` tile 需要 rescale。
+
+后续 block 必须更小心。WG2 只有在跳过不必要 rescale 或完成旧 `O` 的 rescale 后,才 arrive `p_o_rescale`。Skip test 故意保守:softmax 计算 log2-scaled delta `(m_old - m_new) * scale_log2`;如果这个值仍高于 `-rescale_threshold`,新 max 没有移动到值得 rescale 的程度,因此 kernel 保留旧 max,并把 `acc_scale` 精确设为 1.0。只有更大的 max jump 才会走 `exp2` 路径,并要求 WG2 rescale `O`。
+
+WG2 随后用 `any_sync` 在 warpgroup 内 reduce `should_rescale`。如果没有 row 需要更新,它就让 `O` 保持原样。这个 skip 很重要,因为 rescale `O` 是覆盖整个 accumulator 的完整 TMEM -> RF -> TMEM read-modify-write;当 threshold logic 已经把 `acc_scale` 保持在 1.0 时,这就是纯浪费。
+
+注意,所有新 barrier 都聚集在一个地方。`s_ready`、`p_o_rescale`、`p_ready_2` 和 softmax/correction pair 都是 softmax 周围的 barrier。它们存在的唯一原因是:score MMA 和 value MMA 不再相邻。Register math、TMEM rewrite 和 output rescaling 现在位于两者之间,每一步都需要自己的 handoff。
+
+**Try with your agent**:让它 trace 一个 K/V block 穿过 `s_ready`、`p_o_rescale`、`p_ready_2` 和 `o_ready`。对每个 barrier,问谁 wait、谁 arrive、哪个 tile 变得可读,以及之后哪个 storage 可以复用。
+
+## Pipelining Structure
+
+Barrier 告诉我们角色消费 tile 前必须有什么*ready*。但它们没有告诉我们实际有哪些东西*并发*运行,这正是现在要讨论的问题。二者确实不同:一个 correctness gate 可能在 producer 实际运行之前很久或之后很久才满足。
+
+这里没有单一 pipeline depth,因为不同 tile stream 以不同速度移动。因此 kernel 为每类 stream 保持单独的 ring:
+
+- Q pipeline depth 2:一个 CTA 处理两个 Q stage。WG0 处理一个 stage,WG1 处理另一个。
+- KV pipeline depth 3:K 和 V block 在 inner loop 中 streaming,而同一批 Q stage 会复用。
+- TMEM pipeline depth 2:每个 Q stage 有自己的 S/P/O TMEM slot,这些 slot 会在匹配 barrier 触发后复用。
+
+下图从 correctness gate 切换到 timeline view,展示这些独立 ring 进入飞行状态后,哪些角色可以大致同时 active。
+
+
+
+把它读作 timeline,而不是 barrier graph。它展示大致同一时间哪些角色 active;前面的 barrier-flow 图则用于检查精确 producer-consumer wait。两张图分别回答本节开头提出的两个不同问题。
+
+每一行对应代码中的一个 role branch:
+
+- WG3 warp 1 发起 TMA load。
+- WG3 warp 0 发起 score MMA 和 value MMA。
+- WG0 和 WG1 为两个 Q stage 运行 softmax。
+- WG2 release 或 rescale `O`,稍后 normalize 最终输出。
+- WG3 warp 2 发起 TMA store。
+
+从左到右沿图走,可以看到一个代表性的 pipeline wave。Load warp 从 `Q0`、`K[n-1]`、`Q1`、`V[n-1]` 开始,然后继续 stream 更低索引的 K/V block。MMA warp 发起第一批 score MMA 来产生 `S0` 和 `S1`,WG0/WG1 把它们变成 `P0` 和 `P1`。
+
+重要的是,MMA warp 并不会先运行所有 score MMA,再运行所有 value MMA。一旦两个 Q stage 都 primed,它就会交错两类 MMA:当前 `V` block 的 value MMA,然后下一个 `K` block 的 score MMA,如此继续:
+
+```text
+score Q0*K[n-1]
+score Q1*K[n-1]
+value P0*V[n-1]
+score Q0*K[n-2]
+value P1*V[n-1]
+score Q1*K[n-2]
+value P0*V[n-2]
+...
+```
+
+这种 interleaving 正是图中 score、softmax、correction 和 value 行重叠,而不是整齐依次运行的原因。
+
+WG2 行标注为 `release / rescale`,两个半部分对应我们已经见过的两种情况。第一个 K/V block 上还没有旧 `O`,因此 WG2 只参与让 value MMA 继续的 handoff;后续 block 上,它可能在 value MMA 累加前 rescale 旧 `O`。Normalization 和 TMA store 只在 attention task 的最后一个 K/V block 之后发生一次。
+
+没有单个 GEMM-style pipeline 可以描述 FA4,因为 Q、K/V 和 TMEM slot 都按独立 schedule 前进。TIRx 把这些 schedule 显式保留下来,表现为单独的 tile buffer、`PipelineState` cursor 和 barrier phase,而不是把 kernel 藏在一个 monolithic primitive 后面。代价是 moving parts 更多;收益是复杂性保持可见、可检查。
+
+## Rescaling 和 Writeback
+
+Rescale 是强制需要的,不是可以去掉的优化。Online softmax 可能随着每个新 score tile 抬高 per-row maximum;一旦抬高,早期 block 累加出的 `O` 是按*旧* maximum 缩放的。这会让早期每一项都比正确值大 `exp(m_new - m_old)` 倍。跳过 correction 后,这些 block 会被过度加权,最终输出就是错的。修复是一次 TMEM -> registers -> TMEM tile operation:
+
+$$O_{\text{old}} \leftarrow O_{\text{old}} \cdot e^{(m_{\text{old}} - m_{\text{new}}) / \sqrt{d}}$$
+
+工作由两个角色拆分。Softmax 计算 per-row scale,并把它放入 SMEM mailbox;WG2 等待 `softmax_corr.full`,从 TMEM 读出当前 `O`,乘上这个 scale,再把 `O` 写回:
+
+```python
+RESCALE_TILE = T.meta_var(16)
+o_row = T.wg_reg_tile(RESCALE_TILE)
+Tx.copy_async(o_row, O_region[i_q, d_start : d_start + RESCALE_TILE])
+Tx.mul(o_row, o_row, acc_scale)
+Tx.copy_async(O_region[i_q, d_start : d_start + RESCALE_TILE], o_row)
+T.ptx.tcgen05.wait.st()
+```
+
+需要强调,这是覆盖完整 `O` accumulator 的 TMEM -> registers -> TMEM tile operation,不是一点 scalar bookkeeping,它和其他 stage 一样有 readout card:
+
+> **Tile-primitive readout:Correction(rescale)**
+> - Scope:WG2,完整 warpgroup。
+> - Layout:TMEM 中的 `O` -> registers -> TMEM 中的 `O`(`O_region[i_q]`)。
+> - Dispatch:用 `tcgen05.ld` 读取,用 TMEM store 写入;中间做 register multiply。
+> - Handoff:等待 `softmax_corr.full`;arrive `p_o_rescale`(-> value MMA)和 `softmax_corr.empty`(-> softmax)。
+
+端到端同步如下:
+
+1. Softmax 把 scale value 写入 SMEM。
+2. WG2 等待 `softmax_corr.full`。
+3. WG2 在 TMEM 中 rescale `O`。
+4. WG2 arrive `p_o_rescale`。
+5. WG3 的 value MMA 现在可以消费 `P` 并累加到 rescaled `O` tile 中。
+
+WG2 读取完 SMEM slot 后,`softmax_corr.empty` release 这个 slot,loop 闭合,softmax 可以在下一次 iteration 复用 mailbox。
+
+K/V loop 结束后,WG2 从 correction 切换到 epilogue。它等待最终 `row_sum` 和 `o_ready`,从 TMEM 读取最终 `O`,乘以 `1 / row_sum`(最开始被我们推迟的 normalization),cast 到 fp16,并写入 `O_smem`。WG3 的 TMA store warp 随后把 `O_smem` 带回 GMEM。
+
+对打算扩展这个 kernel 的人,有一个限制值得标明。它只计算 forward output,而 training forward pass 通常还会保存 backward pass 需要的 log-sum-exp(LSE)。加入 LSE 时有一个 scaling 细节要记住:这个 kernel 把 `row_max` 保持为*未缩放* raw `QK^T` score 的最大值,而 `row_sum` 累加的是 `exp((S - row_max) / sqrt(d))`。因此形成 natural-log LSE 时,必须把 `1/\sqrt{d}` 因子重新应用到 `row_max` 上:
+
+$$\mathrm{LSE}_i = \log(\mathrm{row\_sum}_i) + \mathrm{row\_max}_i / \sqrt{d}$$
+
+这个实现只输出 forward result,不写 LSE。
+
+## Causal Masking
+
+Causal attention 增加了一个约束:query 只能 attend 到自己位置及之前的 key。Kernel 用两个互补方式满足这个约束,一个便宜,一个精确。
+
+便宜方式是完全跳过 work。很多 K/V block 对某个 Q block 来说完全位于 diagonal 上方,没有任何贡献,因此 `get_n_block_max(...)` 会计算这个 block 可能需要的最后一个 block,loop 直接不加载也不计算剩下部分。
+
+精确方式处理跨越 diagonal 的 block,其中有些 column 有效,有些无效。这些 block 仍然运行 score MMA,但 softmax 会在 exponentiation 前 mask 掉无效 column。对于每一行,它从该行 query 位置和 block offset 推导 column limit,保留不超过这个 limit 的 column,并把超过它的每个 column 在寄存器中设为 `-inf`,因此这些 column 不会贡献 row max,也不会贡献 `exp2` numerator。
+
+实现不会逐元素 branch,而是用 `mask_r2p(...)` 应用这个 limit,把它变成覆盖整个 32-wide score chunk 的 bit mask,并一次性 mask 这个 chunk。完全位于 diagonal 下方的 block 保留所有 column,不需要 mask。
+
+从 tile-primitive 视角看,causal mode 完全不重写数据路径。它只裁剪 K/V trip count,并在 register-resident softmax 中、score MMA 和 `P` writeback 之间插入一个 masking step。
+
+## GQA 支持
+
+Grouped Query Attention 让多个 query head 共享一个 K/V head。这节省 memory bandwidth,但提出一个 packing 问题:如何保持只有一个 K/V tile,同时仍然把许多 query head 喂给它?Kernel 的答案是一次处理一个 query-head group,让它们共同使用一个 scheduled `kv_head_idx`:
+
+```python
+GQA_RATIO = num_qo_heads // num_kv_heads
+SEQ_Q_PER_TILE = BLK_M // GQA_RATIO
+```
+
+技巧是重新解释 128 个 Q-tile row。对于 `GQA_RATIO=4`,它们不再表示 128 个 sequence position;而是表示 32 个 sequence position 乘以 4 个 query head,并打包在一起,使四个 head 搭乘同一个 K/V tile。Row 解码为:
+
+```text
+seq_pos = row // GQA_RATIO
+q_head = row % GQA_RATIO
+```
+
+Q load 用一个 3D view 表达这种 packing。Source 是自然的 `Q[batch, seq, qo_head, dim]` 布局,destination 是稍后 score MMA 会作为 flat `128 x HEAD_DIM` operand 读取的同一个 SMEM tile。View 负责调和两者,而且不需要任何 copy:
+
+```python
+Q_smem_3d = Q_smem.view(SMEM_PIPE_DEPTH_Q, SEQ_Q_PER_TILE, GQA_RATIO, HEAD_DIM)
+Tx.copy_async(
+ Q_smem_3d[i_q, :, :, :],
+ Q[batch_idx,
+ m_start : m_start + SEQ_Q_PER_TILE,
+ kv_head_idx * GQA_RATIO : (kv_head_idx + 1) * GQA_RATIO,
+ :],
+ **tma_copy_q,
+)
+```
+
+K 和 V 从不在内存中扩展,这正是 GQA 的意义:`kv_head_idx` 对应的单个 K/V tile 被打包到 Q row 中的所有 `GQA_RATIO` query head 复用。输出侧镜像输入:epilogue 后用匹配的 3D view 把 packed row store 回 `O[batch, seq, qo_head, dim]`。
+
+结果是 GQA 完全存在于 Q-load 和 O-store 边界。Compute path 内部,score MMA 仍然看到普通的 `128 x HEAD_DIM` Q tile,tile-primitive graph 的其余部分不变。
+
+## Tile Scheduling
+
+Scheduler 的工作是把每个 CTA 映射到一个 `(batch, kv_head, m_block)` attention task,正确策略取决于 masking 是否让这些 task 成本相同:
+
+- Non-causal mode 使用 `FlashAttentionLinearScheduler`。每个 task 工作量相同,因此固定 CTA pool 按 `num_ctas` 前进就足以均匀分配。
+- Causal mode 使用 `FlashAttentionLPTScheduler`,因为 causal masking 会让 work 极不均匀:靠近开头的 Q block 大约只 attend 一个 K/V block,而靠近结尾的 block 会 attend 全部 block。朴素切分会让一些 CTA 比其他 CTA 晚很多完成,所以 longest-processing-time scheduler 会优先安排重 block,使结束时间更均衡,同时仍然把邻近 batch/head task 放在一起以改善 L2 locality。
+
+二者虽然不同,但暴露同样的 loop interface:
+
+```python
+while scheduler.valid():
+ m_block_idx = scheduler.m_block_idx
+ batch_idx = scheduler.batch_idx
+ kv_head_idx = scheduler.head_idx
+ # process one Q block against its K/V block range
+ scheduler.next_tile()
+```
+
+唯一行为差异在 `next_tile()` 做什么:non-causal mode 中它把 CTA 前进到另一个 task;causal mode 中它在当前 task 后结束 loop。无论如何,这只是 scheduling decision:它选择 CTA 拥有*哪个* attention tile,从不改变 tile 如何计算。Loop 内部运行同样的 local primitive:TMA load、score MMA、softmax、value MMA、correction、TMA store。
+
+## 编译和验证
+
+上面都是摘录,所以要把它们合起来实际运行 kernel,我们从 `tirx-kernels` import 真实实现,编译它,并用 torch reference 检查。完整 kernel,即本章讲过的所有部分组合成一个文件,位于 `tirx-kernels` 仓库中的 [`flash_attention4.py`](https://github.com/mlc-ai/tirx-kernels/blob/main/tirx_kernels/attention/flash_attention4.py)。与 GEMM verify cell 有两个不同点:Flash Attention 有更丰富的入口(`get_flash_attention4_kernel`),并且额外接受 `profiler_buf` 参数用于内置 profiler。整个章节只需要运行这一段:
+
+```python
+import torch
+import torch.nn.functional as F
+import tvm
+from tirx_kernels.attention.flash_attention4 import (
+ get_flash_attention4_kernel, PROFILER_BUFFER_SIZE)
+
+B, S, Hq, Hkv, D = 1, 1024, 32, 8, 128 # GQA: 32 query heads share 8 KV heads
+Q = torch.randn(B, S, Hq, D, dtype=torch.float16, device="cuda")
+K = torch.randn(B, S, Hkv, D, dtype=torch.float16, device="cuda")
+V = torch.randn(B, S, Hkv, D, dtype=torch.float16, device="cuda")
+O = torch.empty(B, S, Hq, D, dtype=torch.float16, device="cuda")
+prof = torch.zeros(PROFILER_BUFFER_SIZE, dtype=torch.uint64, device="cuda")
+
+kernel = get_flash_attention4_kernel(B, S, S, Hq, Hkv, D, is_causal=False)
+target = tvm.target.Target("cuda")
+with target:
+ ex = tvm.compile(tvm.IRModule({"main": kernel}), target=target, tir_pipeline="tirx")
+ex.mod(Q, K, V, O, prof) # ex.mod takes torch tensors directly, like every other chapter
+torch.cuda.synchronize()
+
+# torch reference; enable_gqa lets the 32 query heads share the 8 KV heads
+qt, kt, vt = (x.transpose(1, 2).float() for x in (Q, K, V))
+ref = F.scaled_dot_product_attention(qt, kt, vt, enable_gqa=True).transpose(1, 2).half()
+torch.testing.assert_close(O, ref, rtol=1e-2, atol=1e-2)
+print(f"FA4: B={B} S={S} Hq={Hq} Hkv={Hkv} D={D}, non-causal -> PASS")
+```
+
+**期望输出**:`... -> PASS`。Kernel 以 fp32 累加 online softmax,但它的结果与高精度 reference 之间仍然存在多个近似来源。包括 input 和 operand 的 fp16 存储与 rounding;基于 `exp2` 的 softmax 重写(把每个 exponential 重写成 `scale_log2 = log2(e)/√d` 形式);online-softmax 的重排和 per-row rescaling,它在 running scale 中分块求和,而不是一次性求和;最后还有 writeback 时对 `O` 的 fp16 cast。这里选择的 `rtol`/`atol` 与源码 kernel 自己测试使用的 tolerance 相同,是为了同时覆盖这些因素相对 torch reference 的误差,而不是只覆盖 fp16 rounding。所以如果这里出现真正 failure,而不是边缘 near-miss,请把它理解成指向 softmax path 的路标:可能漏了 `s_ready` / `p_o_rescale` / `p_ready_2` wait,或者 `row_max` / `row_sum` 更新没有正确应用到 rescale step。这些正是本章花大量 barrier 讨论的 handoff。
+
+## 与 GEMM 的差异
+
+下表沿变化的轴比较 FA4 和 GEMM:
+
+| Aspect | GEMM | Flash Attention 4 |
+|--------|------|-------------------|
+| MMA phases | 一个重复的 MMA | score MMA 和 value MMA |
+| Work between MMAs | 除 pipeline handoff 外没有 | online softmax、masking 和 O rescaling |
+| Running state | 只有 accumulator | row max、row sum、O accumulator |
+| Main intermediate | accumulator TMEM tile | S、P 和 O TMEM tile region |
+| Warp roles | TMA producer、MMA consumer、writeback | TMA load、MMA、softmax、correction、TMA store |
+| Barriers | 主要是 load/compute/writeback handoff | 额外的 score/softmax/value/correction handoff |
+| Scheduling unit | output matrix tile | attention task:`(batch, kv_head, m_block)` |
+
+这些差异都可以追溯到本章开头说的结构变化:第二个 MMA,以及夹在两个 MMA 之间的 softmax。底层 TIRx contract 完全没有改变:
+
+- tile primitive 说明哪个 tile 被移动或计算,
+- 周围 scope 说明哪些线程协作,
+- layout 说明 tile 位于哪里,
+- barrier 说明下一个角色何时可以消费它。
+
+因此,FA4 比 GEMM 难,不是因为它依赖不同硬件,而是因为 tile value 更多、它们之间的 handoff 更多。
+
+## 练习
+
+1. 与 GEMM 相比,FA4 的两个 MMA phase 之间出现了什么新的 tile handoff?命名 producer、TMEM tile 和 consumer。
+2. 为什么 softmax 要把 numerator tile `P` 写回 TMEM,而不是只把它保存在寄存器里供 value MMA 使用?
+3. 选择 `p_o_rescale` 或 `p_ready_2`。这个 barrier 精确证明了什么?如果 value MMA 跳过这个 wait,会出什么问题?
-本页用于放置 Flash Attention 4 kernel 构建过程的中文翻译。
+**Try with your agent**:选择一个没有标注的 tile primitive,例如 epilogue 中的 `Tx.copy_async`、fp32 -> fp16 的 `Tx.cast`,或第二个 `gemm_pv` sub-MMA。让它给出 scope / layout / dispatch / handoff 卡片,然后对照源码中的 guard、allocation 和 wait 检查答案。
diff --git a/zh/chapter_gemm_advanced/index.md b/zh/chapter_gemm_advanced/index.md
index 21ef23c0..61833874 100644
--- a/zh/chapter_gemm_advanced/index.md
+++ b/zh/chapter_gemm_advanced/index.md
@@ -1,13 +1,910 @@
----
-orphan: true
----
-
(chap_gemm_advanced)=
+# 用 Warp Specialization 和 Cluster 扩展 GEMM
+
+:::{admonition} 概览
+:class: overview
+
+- Pipelined GEMM 仍然让一个 warpgroup 按顺序执行 load、MMA 和 writeback,本章要移除的瓶颈正是这一点。
+- Step 7 把 warp specialization 成不同角色,Step 8 加入 2-CTA cluster,Step 9 加入多个 consumer。
+- 每一步都移除一个串行瓶颈,最终达到接近 state-of-the-art 的吞吐。
+:::
+
+上一章({ref}`chap_gemm_async`)中的 pipelined GEMM 已经很快,但它仍然要求一个 warpgroup 做所有事:发起 load、运行 MMA、再把结果写回。即使有 software pipeline,这一组线程仍然是三个硬件引擎汇合的地方。
+
+症状很容易看出来。Tensor Core 运行时 TMA unit 变安静,结果写回 memory 时 Tensor Core 变安静,每个引擎都通过同一组线程等待其他引擎。突破这个问题的方式,是不再让一组线程做所有事。
+
+我们通过三个逐步扩大协作范围的 step 来贯彻这个想法。Step 7({ref}`chap_warp_specialization`)把 warp specialization 成 producer、consumer 和 writeback 角色。Step 8({ref}`chap_cta_cluster`)把两个 CTA 组成一个 cluster,并让它们跨 shared memory 共享 operand。Step 9({ref}`chap_multi_consumer`)加入第二个 MMA consumer,让一个 staged tile 喂给两份数学计算。
+
+把这三个 step 看作同一个 pattern 在不同尺度上的应用会很有帮助。Step 7 把完整 pipeline 保持在一个 CTA 内部:TMA 和 MMA 共享一个 warpgroup,而 writeback 在另一个 warpgroup 中运行。Step 8 把协作扩大到 CTA 之间,产生跨越两个 CTA 的 256×256 tile。Step 9 进一步提高 compute density:cluster output 增长到 512×256,每个 staged B tile 被两个 consumer 复用,我们到达本教程中最密集的变体。
+
+贯穿这些变化的有一件事保持不变。SMEM、TMEM 和 register layout 仍然遵守前两章建立的 contract;变化的是*谁协作*,而不是数据如何布局。Step 8 是合作 scope 第一次越过单个 CTA,因此 operand tile 会切分到两个 CTA 的 shared memory 中,并且一个布局会沿 `cbx` cluster 轴跨越两个 CTA。
+
+
(chap_warp_specialization)=
+## Step 7:Warp Specialization + Pipeline
+
+Single-warpgroup kernel 留下性能的原因很简单:每个线程都走同一条路径,先 load,再 compute,再 write。所以在它 load 时 Tensor Core 没事做,在它 compute 时 TMA engine 没事做。修复方式是 *warp specialization*。我们不再要求一组线程依次做每项工作,而是把每项工作交给专门的 warp,并让这些 warp 同时运行,中间由 software pipeline 串接。这是 GEMM 路径中最大的架构变化,本章剩余部分都建立在它之上。本节 benchmark 使用 M=N=K=4096。
+
+> **本 step 改变的内容:Scope**
+> - Scope:一个 warpgroup 顺序执行 load -> MMA -> writeback,变成三个并发角色(TMA producer、MMA consumer、writeback),并由 full/empty barrier 连接。
+> - Layout:不变,仍然是 Step 6 中的 SMEM stage 和 TMEM accumulator。
+> - Dispatch:不变,TMA load 和 `tcgen05` MMA。
+
+**主题。**
+
+- Warp specialization:把不同 warp/warpgroup 专门分配给不同任务
+
+- 高层 barrier abstraction:`TMABar`、`TCGen05Bar`、`MBarrier`
+
+- 用 `PipelineState` 自动管理 stage/phase
+
+- 用于 per-warpgroup synchronization 的 `warpgroup_sync` barrier ID
+
+(多 stage SMEM pipeline 和 persistent `ClusterPersistentScheduler2D` 会从 Steps 5-6 原样复用;这里只新增 scope split。)
+
+### 从顺序到并发
+
+在介绍角色和 barrier 之前,先隔离 warp specialization 要移除的 scheduling bottleneck 会更清楚。下图用 Step-4-style sequential timeline 紧凑表示 Steps 4-6 中 specialization 前的 kernel,然后把它放在 Step 7 warp-specialized schedule 上方,让 engine utilization 的差异一眼可见。
+
+
+
+上方是 specialization 前的 single-warpgroup pattern:同一组未 specialization 的线程同时拥有 load path 和 MMA path,所以一个 engine 在另一个 active 时很容易 idle。Steps 5 和 6 通过 double buffering 和 persistent scheduling 改善这个 baseline,但它们还没有把 loading 和 compute 拆成独立 producer/consumer 角色。下方的 specialization 打破了这种轮流执行。TMA producer 在 MMA consumer 忙于计算时 prefetch 下一个 tile,writeback 独立前进。Producer warp 3 在 consumer warp 0 仍在处理当前 MMA 时发起下一次 load,因此两个 engine 都不必等待对方。Load/MMA 交接使用两个 barrier:
+
+- **`tma2mma`**(TMA -> MMA):表示已加载的 SMEM 数据已经准备好给 MMA 消费。
+- **`mma2tma`**(MMA -> TMA):表示 MMA 已经读完一个 buffer,TMA 可以把它复用于下一次 load。
+
+图中有一个细节第一眼看起来像错误:`mma2tma` 箭头会跳过一个 stage。原因是 ring buffer。`PIPE_DEPTH=2` 时有两个 SMEM buffer,stage 0 和 stage 1;TMA Load k=0 填充 buffer 0,TMA Load k=1 填充 buffer 1。当 MMA Compute k=0 读完 buffer 0 后,它 signal `mma2tma` 表示这个 buffer 空闲,但真正想要拿回 buffer 0 的 load 是 TMA Load k=2,而不是 k=1(它正在使用 buffer 1)。因此从 MMA Compute k=0 发出的 `mma2tma` 箭头会一直指向 TMA Load k=2。Release 跳过一个 stage,只是因为这个 ring 有两个 slot。
+
+### Warp 角色
+
+Timeline 展示了*为什么*要拆分 work;下一个问题是*谁*做每个部分。Specialization 把 load、compute、writeback 三项工作分配给具体 warp,使它们可以同时运行。使用 `WG_NUMBER=2` 时,kernel 使用两个 warpgroup(角色表中缩写为 WG):
+
+| Actor | Location | Job |
+|-------|----------|-----|
+| **TMA Producer** | Warpgroup 1, warp 3 | 持续通过 TMA 加载 A 和 B tile |
+| **MMA Consumer** | Warpgroup 1, warp 0 | 数据就绪后立即运行 MMA |
+| **Writeback** | Warpgroup 0(所有 warp) | 读取 TMEM 结果并写入 GMEM |
+
+### 4 个 Barrier
+
+三个并发 actor 需要四个 barrier,这四个 barrier 可以整齐地分成两个方向。前向路径(TMA -> MMA -> Writeback)发出数据*就绪*信号;它传递的信息是“你等待的 tile 已经到了”。反向路径(Writeback -> MMA -> TMA)发出 buffer *release* 信号:“你想要的 slot 又空出来了”。理解命名约定后,名字本身就能读懂:每个名字都是 `source2destination`,所以 `tma2mma` 就是 TMA 向 MMA 发信号的 barrier。
+
+| Barrier | Type | Direction | Meaning |
+|---------|------|-----------|---------|
+| **tma2mma** | `TMABar` | TMA -> MMA | “SMEM data is ready” |
+| **mma2tma** | `TCGen05Bar` | MMA -> TMA | “SMEM buffer can be reused” |
+| **mma2ld** | `TCGen05Bar` | MMA -> Writeback | “TMEM results are ready” |
+| **ld2mma** | `MBarrier` | Writeback -> MMA | “TMEM is free for next tile” |
+
+为什么每个 barrier 有对应的 *type*?Type 来自 producer 宣布完成的方式。**TMA load** 使用 `TMABar`,也就是带 byte counting 的 mbarrier:TMA hardware 自己会在 transfer byte 落地后 arrive 到 barrier,consumer 因此无需 thread polling 就能知道数据就绪。**TMA store** 不能使用这个方式(store 没有要通知的对象),所以它回到 `cp_async.bulk.commit_group()` + `wait_group(0)`,由 issuing thread 等待自己的写入 drain。**MMA operation** 使用 `TCGen05Bar`,其中 `tcgen05.commit()` 指令会在 MMA 完成时 signal barrier。
+
+这里有个小细节会在 Step 8 派上用场。`arrive` 调用会传入 `cta_mask=0`,因为在 single-CTA kernel 中没有其他 CTA 需要 signal。当 Step 8 形成 cluster 时,正是这个参数变成非零值,并成为唤醒协作 CTA 的机制。
+
+### PipelineState
+
+四个 barrier 告诉角色 buffer *何时*就绪;但还需要有东西跟踪 pipeline 循环时每个角色位于*哪个* buffer 上。这正是 `PipelineState` 管理的 bookkeeping。Ring buffer 同时携带两类 bookkeeping:当前在哪个 slot,以及正在等待这个 slot 的 barrier 的哪个 “phase”。在 pipelined loop 中手工跟踪这两者,正是容易产生 off-by-one error 的地方,而这里的 off-by-one 会让整个 kernel deadlock。`PipelineState` 把两者绑在一起,避免你手写:
+
+```python
+tma_ps = PipelineState(PIPE_DEPTH, phase=1) # Producer starts ready (phase=1)
+# tma_ps.stage = current stage index
+# tma_ps.phase = current phase (0 or 1)
+tma_ps.advance() # Advance to next stage
+```
+
+初始 `phase` 决定某个角色第一次 `wait` 是直接通过还是阻塞,而 pipe 两端正确答案正好相反,这也是容易出错的部分:
+
+- `phase=1`(producer)-> 第一次 `wait(phase=1)` 看到 barrier 仍处于 phase 0,因为 0 != 1,所以会**立即通过**。这正是我们想要的,因为 buffer 初始为空,producer 应该可以立刻开始填充。
+
+- `phase=0`(consumer)-> 第一次 `wait(phase=0)` 看到 barrier 处于 phase 0,因为 0 == 0,所以会**阻塞**。这也是我们想要的,因为还没有数据,consumer 在 producer arrive 前没有东西可读。
+
+如果给两端相同 starting phase,就会得到 deadlock,或者更糟的静默 corruption,因此这个选择值得认真对待。
+
+### `warpgroup_sync` Barrier ID
+
+Specialization 会引入一个很容易踩到的 synchronization hazard。一旦每个 warpgroup 走不同 code path,熟悉的 `cta_sync()` 就会 deadlock:它使用硬件 barrier #0,并要求*每个* CTA thread 都 arrive,但在 warpgroup branch 内只有部分线程存在。我们需要的是作用域限定在单个 warpgroup 内的 barrier。GPU 提供 16 个 named barrier(ID 0-15),所以 kernel 使用 `warpgroup_sync(10)`,它只同步一个 warpgroup 内的线程。当多个 warpgroup 各自需要同步时,例如 multi-consumer Step 9,会通过 `warpgroup_sync(wg_id + 10)` 使用不同 ID,避免撞到同一个硬件 barrier。
+
+**实现。**
+
+这里使用 `PIPE_DEPTH=2`,这是仍能让 load 和 compute 重叠的最小深度。更深 pipeline 可以隐藏更多 memory latency,直到 SMEM 预算达到上限;下面的 *When Step 7 misbehaves* 会详细讨论这个取舍。现在所有组件都齐了:角色、四个 barrier、`PipelineState` 和 warpgroup-scoped sync,可以组合出完整 kernel:
+
+```python
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+from tvm.tirx.lang.pipeline import TMABar, TCGen05Bar, MBarrier, PipelineState
+from tvm.tirx.lang.tile_scheduler import ClusterPersistentScheduler2D
+
+SM_COUNT = 148 # Number of SMs on NVIDIA B200 GPU
+F16_SIZE = 2
+
+def hgemm_v7(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ K_TILES = K // BLK_K
+ PIPE_DEPTH = 2
+ WG_NUMBER = 2
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (PIPE_DEPTH, BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (PIPE_DEPTH, BLK_N, BLK_K))
+ D_layout = tma_shared_layout(d_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_N))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ bx = T.cta_id([SM_COUNT])
+ wg_id = T.warpgroup_id([WG_NUMBER])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- Allocation ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ tma2mma = TMABar(pool, PIPE_DEPTH)
+ mma2tma = TCGen05Bar(pool, PIPE_DEPTH)
+ mma2ld = TCGen05Bar(pool, 1)
+ ld2mma = MBarrier(pool, 1)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((PIPE_DEPTH, BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((PIPE_DEPTH, BLK_N, BLK_K), b_type, layout=B_layout)
+ Dsmem = pool.alloc((BLK_M, BLK_N), d_type, layout=D_layout)
+
+ # --- Barrier init ---
+ tma2mma.init(1)
+ mma2tma.init(1)
+ mma2ld.init(1)
+ ld2mma.init(128) # all 128 Warpgroup 0 threads arrive
+ pool.commit()
+
+ # --- TMEM alloc + fence ---
+ if wg_id == 0:
+ if warp_id == 0:
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), acc_type, scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)]))
+
+ # --- Tile scheduler ---
+ tile_scheduler = ClusterPersistentScheduler2D(
+ "ts", num_m_tiles=M // BLK_M, num_n_tiles=N // BLK_N,
+ l2_group_size=8, num_clusters=SM_COUNT)
+ tile_scheduler.init(bx)
+ m_st = T.meta_var(tile_scheduler.m_idx * BLK_M)
+ n_st = T.meta_var(tile_scheduler.n_idx * BLK_N)
+
+ # =============================================
+ # Warpgroup 1: TMA Producer (warp 3) + MMA Consumer (warp 0)
+ # =============================================
+ if wg_id == 1:
+ if warp_id == 3:
+ # === TMA Producer ===
+ tma_ps = PipelineState(PIPE_DEPTH, phase=1)
+
+ @T.inline
+ def tma_load(k_offset):
+ Tx.copy_async(Asmem[tma_ps.stage, :, :],
+ A[m_st:m_st+BLK_M, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=1,
+ mbar=tma2mma.ptr_to([tma_ps.stage]))
+ Tx.copy_async(Bsmem[tma_ps.stage, :, :],
+ B[n_st:n_st+BLK_N, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=1,
+ mbar=tma2mma.ptr_to([tma_ps.stage]))
+
+ if T.filter(lane_id, T.ptx.elect_sync()):
+ while tile_scheduler.valid():
+ for k in range(K_TILES):
+ mma2tma.wait(tma_ps.stage, tma_ps.phase)
+ tma_load(k * BLK_K)
+ tma2mma.arrive(tma_ps.stage,
+ (BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE)
+ tma_ps.advance()
+ tile_scheduler.next_tile()
+
+ elif warp_id == 0:
+ # === MMA Consumer ===
+ mma_ps = PipelineState(PIPE_DEPTH, phase=0)
+ ld_ps = PipelineState(1, phase=1)
+
+ if T.filter(lane_id, T.ptx.elect_sync()):
+ while tile_scheduler.valid():
+ # Wait for TMEM to be free from previous tile's writeback
+ ld2mma.wait(ld_ps.stage, ld_ps.phase)
+ ld_ps.advance()
+
+ for k in range(K_TILES):
+ tma2mma.wait(mma_ps.stage, mma_ps.phase)
+ Tx.gemm_async(
+ tmem[:, :BLK_N],
+ Asmem[mma_ps.stage, :, :],
+ Bsmem[mma_ps.stage, :, :],
+ accum=(k != 0), dispatch="tcgen05", cta_group=1)
+ mma2tma.arrive(mma_ps.stage, cta_group=1, cta_mask=0)
+ mma_ps.advance()
+
+ # Signal results ready for writeback
+ mma2ld.arrive(0, cta_group=1, cta_mask=0)
+ tile_scheduler.next_tile()
+
+ # =============================================
+ # Warpgroup 0: Writeback
+ # =============================================
+ elif wg_id == 0:
+ wb_ps = PipelineState(1, phase=0)
+ reg_f16 = T.alloc_local((BLK_N,), d_type)
+
+ while tile_scheduler.valid():
+ # Wait for MMA results
+ mma2ld.wait(wb_ps.stage, wb_ps.phase)
+ wb_ps.advance()
+
+ # Read TMEM -> registers (warpgroup scope)
+ reg = T.alloc_local((BLK_N,), acc_type)
+ reg_wg = reg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+ Tx.wg.copy_async(reg_wg[:], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+
+ # Signal TMEM free (all 128 threads arrive)
+ ld2mma.arrive(0, cta_id=0, pred=True)
+
+ # Cast fp32 -> fp16
+ Tx.cast(reg_f16[:], reg[:])
+
+ # Write to Dsmem + TMA store
+ Tx.copy(Dsmem[warp_id * 32 + lane_id, :], reg_f16[:])
+ T.ptx.fence.proxy_async("shared::cta")
+ T.cuda.warpgroup_sync(10)
+ if warp_id == 0:
+ if lane_id == 0:
+ Tx.copy_async(D[m_st:m_st+BLK_M, n_st:n_st+BLK_N],
+ Dsmem[:, :], dispatch="tma")
+ T.ptx.cp_async.bulk.commit_group()
+ T.ptx.cp_async.bulk.wait_group(0)
+ T.cuda.warpgroup_sync(10)
+
+ tile_scheduler.next_tile()
+
+ # --- Cleanup ---
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
+
+要运行这些 kernel,复用 Step 1 中展示过的 compile / run / check harness({ref}`chap_gemm_basics`):把 `hgemm_v1` 换成 `hgemm_v7`、`hgemm_v8` 或 `hgemm_v9`,并选择类似 `M=N=K=4096` 的问题规模。注意 clustered step 要求 `M` 和 `N` 是 cluster tile 的倍数(Step 8 是 `256×256`,Step 9 是 `512×256`),所以很小的 `128×128` 尺寸不会产生任何 tile。每个新的 Python session 只编译一个 step;切换 step 前重启 kernel,因为这些 kernel 会复用内部名字,而编译器持有 per-session state。各 step 的 timing 汇总在下面的 *End-to-End Result* 中。
+
+### Epilogue(Writeback)细节
+
+Step 7 可以使用一个很简单的 epilogue。由于只有 `BLK_N=128` 列,writeback warpgroup 可以一次把整个 TMEM tile 读入寄存器,然后发起一次 TMA store。Steps 8 和 9 没有这个便利,这也是它们后面要引入 chunking 的原因;但目前序列是:
+
+1. 等待 MMA:`mma2ld.wait(phase)`。本教程中的 Steps 8 和 9 会在这里加一个 `fence.after_thread_sync()` 作为保守额外操作;MMA-completion mbarrier 已经覆盖 ordering,大多数 kernel(包括 CUTLASS)会省略它,所以 Step 7 也省略。
+2. 读取 TMEM -> registers(每个线程 128 个 fp32,warpgroup scope,通过 `Tx.copy_async(reg_wg, tmem[:, :BLK_N])` 后接 `T.ptx.tcgen05.wait.ld()`)。
+3. Signal MMA:`ld2mma.arrive(0, cta_id=0, pred=True)`(全部 128 个线程 arrive);TMEM 现在可以给下一个 tile 复用。两个 `arrive` kwarg 会在 clustered step 中再次出现:`cta_id` 指定 signal *哪个 CTA 的* barrier copy(`0` = 当前 CTA,也就是 local barrier;Step 8 中 cooperative arrive 会改为通过 `cta_mask` 指向 CTA-0),`pred` 是 per-thread predicate,决定这个线程是否真的 arrive(这里是 `True`,所以每个 writeback thread 都计入 arrival total)。
+4. 在寄存器中 cast fp32 -> fp16。
+5. 写 registers -> Dsmem,然后用 `fence.proxy_async("shared::cta") + warpgroup_sync(10)` flush。
+6. 通过 `cp_async.bulk.commit_group() + wait_group(0)` 执行 TMA store Dsmem -> GMEM。
+
+Step 8(`BLK_N=256`)和 Step 9(每个 consumer 的 `MMA_N=256`)无法保持这种 one-pass 形式,原因是寄存器压力。每个线程读取 256 个 fp32 值,意味着每个线程的寄存器中同时要保存 256 × 4 = 1024 byte,这可能 spill 到 local memory,并且还会迫使 Dsmem buffer 变大。因此这些 step 会把 writeback 拆成 `EPI_N` 列的 chunk(`EPI_N=64`):每次 iteration 只保持 `EPI_N` 个 fp32 register live,并发起对应更小的 TMA store,用更多 store 指令换取舒适的寄存器预算。
+
+**实现备注。**
+
+- **Persistent kernel**:`bx = T.cta_id([SM_COUNT])`,每个 SM 一个 CTA,循环处理多个 tile
+
+- **L2-friendly scheduling**:`ClusterPersistentScheduler2D` 按 cache locality 排列 tile
+
+- 这种 pattern,即 warp specialization 加 software pipelining,在高性能 GEMM kernel 中很常见,包括 CUTLASS 风格设计。
+
+### 当 Step 7 出问题时
+
+Step 7 是第一个让 TMA load、`tcgen05` MMA 和 writeback 同时在飞行中的 GEMM kernel。同样的 failure pattern 会在 Steps 8 和 9 中反复出现:barrier count 不匹配、role guard 放错位置、缺少 fence,或者 staging buffer 在 TMA store drain 之前被复用。用于这些情况的调试 checklist 收集在 {ref}`chap_warp_spec_debug` 中。
+
+**Pipeline depth tuning。** Step 7 kernel 使用最小的 `PIPE_DEPTH=2`。把它推到 4 或 6 可以让 TMA producer 更早跑在 MMA consumer 前面,隐藏更多 memory latency,但代价是消耗更多 SMEM,而 SMEM 有限。B200 每个 SM 提供 228 KB(见 {ref}`chap_background` 中的 *Numbers to Keep in Mind*)。在 `BLK_M=BLK_N=128, BLK_K=64, fp16` 下,每个 pipeline stage 的 A 和 B 合起来消耗 `(128*64 + 128*64) * 2 = 32 KB`,`Dsmem` writeback staging buffer 额外增加 32 KB。因此 `PIPE_DEPTH=4` 大约是 160 KB,`PIPE_DEPTH=6` 大约是 224 KB,已经贴近预算。想更深,就必须重新考虑 writeback staging 策略。
+
+---
+
+Warp specialization 让一个 CTA 内的线程开始协作。下一步会把协作范围扩大到 CTA 边界之外,让两个 CTA 为一个更大的 tile 工作。
+
+
(chap_cta_cluster)=
+## Step 8:2-CTA Cluster
+
+Step 7 让各个 engine 重叠起来,但每个 CTA 仍然独立计算自己的 128×128 tile,并重新加载邻居无法借用的 operand。Step 8 打破这种隔离。两个 CTA 组成一个 cluster,并获得访问彼此 shared memory 的能力,因此单个 cooperative `tcgen05` MMA 可以产生跨越两个 CTA 的 256×256 tile,而一次 B load 现在可以喂给两倍的 MMA work。和之前一样,M=N=K=4096。
+
+> **本 step 改变的内容:Scope + Layout + Dispatch**
+> - Scope:协作范围现在跨越 cluster 中的两个 CTA,而不是一个 CTA。
+> - Layout:operand tile 切分到两个 CTA 的 SMEM 中;CTA 0 拥有共享 completion barrier(`remote_view`)。
+> - Dispatch:MMA 获得 `cta_group` / `cta_mask`,使 `tcgen05` 作为 2-CTA cooperative op 运行。
+
+**主题。**
+
+- CTA cluster:多个 CTA 协作处理更大的 tile
+
+- 通过 `map_shared_rank` 做 cross-CTA SMEM access
+
+- 用 `cta_group=2` 在 256x256 cluster tile 上执行 cooperative MMA
+
+- 用 `cta_mask` 做 cross-CTA barrier signaling
+
+
+### Cluster Tile Shape
+
+整个优化建立在一个硬件能力上:使用 `cta_group=2` 时,MMA 可以读取*两个* CTA staged 的 operand tile,而不仅仅是自己所在 CTA 的 tile。每个 CTA 加载 stored B 的一个 128-row slice,经过 transpose 后,它会变成 128 个逻辑 output column;cooperative MMA 会把两个 slice 重新拼成一个 operand。下图展示两个 CTA 的 A 和 B slice 如何组合成一个 256×256 cluster tile:
+
+```{raw} html
+
+
+
+```
+*交互图:每个 CTA 拥有一个 A row slice 和一个 stored-B row slice,然后通过 cluster(DSMEM)读取另一个 CTA 的 stored-B slice。经过 `B.T` 后,两个 stored-B slice 覆盖完整 output-column span,因此这一对 CTA 产生一个 256×256 output tile。*
+
+**为什么 A 和 B 要在 cluster 中切分**:要理解 256×256 tile 如何 partition,回想本教程把 GEMM 写成 `D = A @ B.T`,其中 stored B 的 shape 是 `N x K`。两个 CTA 组成 cluster 后,切分方式很自然:
+
+- **A 竖直切分**:CTA-0 持有 A0(rows 0-127),CTA-1 持有 A1(rows 128-255)。叠起来是 `[A0; A1]`(256 行)。
+- **Stored B 按 row 切分**:CTA-0 加载 B rows 0-127,CTA-1 加载 B rows 128-255。因为数学使用 `B.T`,这两个 stored row slice 会变成逻辑右操作数的两个 128-column slice。
+- 使用 `cta_group=2` 时,MMA 硬件通过 cross-CTA shared memory access 从**两个** CTA 的 SMEM 中读取 B,因此它看到完整的逻辑 output-column span。
+- 结果:两个 CTA 协作处理一个 256x256 output tile。每个 CTA 写这个 tile 的一个 128x256 row stripe。
+
+这里值得停一下,看看为什么这是真正收益而不只是 work 重新排列。每个 CTA 仍然只加载 128×K 的 A 和 128×K 的 B,因此整个 cluster staged 的 operand 大约是单个 CTA 的 2×,但它产生的是 256×256 tile,拥有 128×128 tile 大约 4× 的 output FLOP。MMA 因此对每个 staged-operand byte 做大约两倍工作,因为每个 CTA 的 B slice 会通过 cooperative MMA 与另一个 CTA 的 A slice 复用。换句话说,算术强度大约翻倍,而这正是仍偏 memory-leaning 的 kernel 需要的杠杆:End-to-End 表中的约 2.2× speedup 来自把同样 byte 喂给更多数学计算。
+
+### Tile Address Calculation
+
+现在 cluster 是 work 的单位,tile scheduler 也必须按 cluster tile 计数。它返回的每个 `(m_idx, n_idx)` 都命名一个完整 256×256 region,而 cluster 内的两个 CTA 会切分这个 region。把 cluster coordinate 转换成每个 CTA 实际加载的 per-CTA slice,如下:
+
+```python
+m_st = (m_idx * CTA_GROUP + cbx) * BLK_M
+n_st = (n_idx * CTA_GROUP + cbx) * BLK_N
+```
+
+两个 CTA 处理的是*同一个* 256×256 cluster tile,单个 coordinate `cbx`(CTA 在 cluster 内的位置,0 或 1)选择这个 CTA 沿两个轴贡献的部分。`m_st` 选择该 CTA 拥有的 output row stripe,`n_st` 选择它喂给 cooperative MMA 的 stored-B slice,writeback 稍后会写出 256-column output span 的两个 128-column half。还要注意,`num_m_tiles = M // 256` 和 `num_n_tiles = N // 256` 计数的是 cluster tile,不是单个 CTA tile。
+
+乍一看,`cbx` 同时出现在 `m_st` 和 `n_st` 中,像是 row offset 泄漏到了 column 里,但两处都是正确的,值得拆开理解。在 writeback 路径上,`cbx` 只属于 M 轴:每个 CTA 拥有不同的 128-row stripe(`m_st = (m_idx * CTA_GROUP + cbx) * BLK_M`,所以 CTA-0 写 rows `m_idx*256 .. +128`,CTA-1 写接下来的 128),但两个 CTA 都写 cluster tile 的*完整* 256 个 output column。因此 store 的 column 来自 cluster 的 `n_idx`(`n_st_epi = n_idx * 256 + no * 128`,完全没有 `cbx`),而不是来自 per-CTA 的 `n_st`。`n_st` 带有 `cbx` 的原因是每个 CTA 要把不同 stored-B row slice 加载进 MMA:在那里,`cbx` 是一个 *load* offset,不是该 CTA 的 output-column offset。
+
+### 与 Step 7 的代码差异
+
+相对 Step 7 的 diff 有六处,每一处都编码了刚才描述的 cluster contract 的一个部分:
+
+```python
+# 1. Cluster launch
+cbx, cby = T.cta_id_in_cluster([CTA_GROUP, 1]) # cbx = CTA index within cluster (0 or 1)
+
+# 2. Cooperative MMA (was cta_group=1)
+Tx.gemm_async(..., cta_group=2)
+
+# 3. Cross-CTA shared memory access
+B_remote = T.ptx.map_shared_rank(Bsmem, cta_id=1)
+
+# 4. Cross-CTA barrier
+tma2mma_cta0 = T.decl_buffer(
+ [CTA_GROUP], "uint64",
+ data=T.ptx.map_shared_rank(tma2mma.ptr_to([0]), 0),
+ scope="shared"
+)
+
+# 5. mma2tma / mma2ld arrives go from cta_mask=0 (single CTA, Step 7)
+# to cta_mask=3 (signal both CTAs in the cluster)
+mma2tma.arrive(mma_ps.stage, cta_group=CTA_GROUP, cta_mask=3)
+mma2ld.arrive(0, cta_group=CTA_GROUP, cta_mask=3)
+
+# 6. Cluster sync replaces cta_sync at the end
+T.cuda.cluster_sync()
+```
+
+
+### Cluster-Scope 变化
+
+这六处修改都来自同一个转变:协作 scope 现在是 cluster,而不是单个 CTA。下面逐项说明这种扩大在实践中意味着什么:每个 CTA 如何找到自己的位置,cluster 通过谁的 barrier 协调,以及哪个 CTA 实际发起 cooperative MMA。
+
+- **Cluster CTA ID**:`cbx` 告诉每个 CTA 自己在 cluster 中的位置(0 或 1)。CTA-0 处理 A rows 0-127,CTA-1 处理 rows 128-255。
+
+- **Remote barrier view**:在一个 cluster 中,每个 CTA 都有自己的 SMEM 和自己的 barrier,这带来一个明显问题:如果 CTA-1 需要等待 CTA-0 产生的东西,它实际触碰谁的 barrier?答案是指定 CTA-0 的 barrier 作为唯一 coordination point,并允许 cluster 中任意 CTA 访问它。`map_shared_rank(tma2mma.ptr_to([0]), 0)` 返回一个指向 CTA-0 barrier 的 cluster-wide pointer;TIRx wrapper `tma2mma.remote_view(0)` 提供这个能力,从此之后每个 arrive 和 wait 都指向 CTA-0 的 copy。
+
+- **MMA dispatch 只来自 CTA-0**:容易把 `cta_group=2` 理解成两个 engine 并行 firing,但实际不是这样。CTA-0 发起恰好一次 `tcgen05.mma`,硬件随后驱动一个跨两个 CTA 的*单个 cooperative* MMA,从两个 SM 的 SMEM 读取 operand,并把 accumulator 写到两个 SM 的 TMEM 中。CTA-1 不发起 MMA。(每个 SM 只有一个 `tcgen05` engine,因此 `cta_group=2` 是一个 cross-SM MMA,而不是两个 engine side by side 运行。)这就是为什么代码用 `if cbx == 0:` guard MMA。
+
+- **Multicast arrive**:`tcgen05.commit(..., cta_group=2, cta_mask=3)` 只由 CTA-0 发起,但会 signal 两个 CTA 的 barrier。`cta_mask=3`(binary `11`)表示目标是 CTA-0 和 CTA-1。
+
+- **ld2mma init count**:`init(128 * CTA_GROUP)`,两个 CTA 的 writeback warpgroup(每个 128 线程)都会 arrive。
+
+
+**实现。**
+
+```python
+def hgemm_v8(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ CTA_GROUP = 2
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ MMA_M, MMA_N = 256, 256
+ K_TILES = K // BLK_K
+ PIPE_DEPTH = 4
+ WG_NUMBER = 2
+ F16_SIZE = 2 # fp16
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (PIPE_DEPTH, BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (PIPE_DEPTH, BLK_N, BLK_K))
+ D_layout = tma_shared_layout(d_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, 128))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ bx = T.cta_id([SM_COUNT])
+ cbx, cby = T.cta_id_in_cluster([CTA_GROUP, 1])
+ wg_id = T.warpgroup_id([WG_NUMBER])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- Allocation ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ tma2mma = TMABar(pool, PIPE_DEPTH)
+ mma2tma = TCGen05Bar(pool, PIPE_DEPTH)
+ mma2ld = TCGen05Bar(pool, 1)
+ ld2mma = MBarrier(pool, 1)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((PIPE_DEPTH, BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((PIPE_DEPTH, BLK_N, BLK_K), b_type, layout=B_layout)
+ Dsmem = pool.alloc((BLK_M, 128), d_type, layout=D_layout)
+
+ # --- Barrier init ---
+ tma2mma.init(1)
+ mma2tma.init(1)
+ mma2ld.init(1)
+ ld2mma.init(128 * CTA_GROUP) # both CTAs' writeback threads
+ pool.commit()
+
+ # --- TMEM alloc (cooperative) ---
+ if wg_id == 0:
+ if warp_id == 0:
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=CTA_GROUP)
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), acc_type, scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)]))
+
+ # --- Tile scheduler (cluster tiles) ---
+ tile_scheduler = ClusterPersistentScheduler2D(
+ "ts", num_m_tiles=M // 256, num_n_tiles=N // 256,
+ l2_group_size=8, num_clusters=SM_COUNT // CTA_GROUP)
+ tile_scheduler.init(bx // CTA_GROUP)
+ m_idx = T.meta_var(tile_scheduler.m_idx)
+ n_idx = T.meta_var(tile_scheduler.n_idx)
+ m_st = T.meta_var((m_idx * CTA_GROUP + cbx) * BLK_M)
+ n_st = T.meta_var((n_idx * CTA_GROUP + cbx) * BLK_N)
+
+ # --- Cross-CTA barrier view ---
+ tma2mma_cta0 = tma2mma.remote_view(0)
+
+ # =============================================
+ # Warpgroup 1: TMA Producer (warp 3) + MMA Consumer (warp 0)
+ # =============================================
+ if wg_id == 1:
+ if warp_id == 3:
+ tma_ps = PipelineState(PIPE_DEPTH, phase=1)
+
+ @T.inline
+ def tma_load(k_offset):
+ Tx.copy_async(Asmem[tma_ps.stage, :, :],
+ A[m_st:m_st+BLK_M, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=CTA_GROUP,
+ mbar=tma2mma_cta0.ptr_to([tma_ps.stage]))
+ Tx.copy_async(Bsmem[tma_ps.stage, :, :],
+ B[n_st:n_st+BLK_N, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=CTA_GROUP,
+ mbar=tma2mma_cta0.ptr_to([tma_ps.stage]))
+
+ if T.filter(lane_id, T.ptx.elect_sync()):
+ while tile_scheduler.valid():
+ for k in range(K_TILES):
+ mma2tma.wait(tma_ps.stage, tma_ps.phase)
+ tma_load(k * BLK_K)
+ if cbx == 0:
+ tma2mma_cta0.arrive(tma_ps.stage,
+ CTA_GROUP * (BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE)
+ tma_ps.advance()
+ tile_scheduler.next_tile()
+
+ elif warp_id == 0:
+ mma_ps = PipelineState(PIPE_DEPTH, phase=0)
+ ld_ps = PipelineState(1, phase=1)
+
+ if cbx == 0:
+ if T.filter(lane_id, T.ptx.elect_sync()):
+ while tile_scheduler.valid():
+ ld2mma.wait(ld_ps.stage, ld_ps.phase)
+ ld_ps.advance()
+
+ for k in range(K_TILES):
+ tma2mma.wait(mma_ps.stage, mma_ps.phase)
+ Tx.gemm_async(
+ tmem[:, :MMA_N],
+ Asmem[mma_ps.stage, :, :],
+ Bsmem[mma_ps.stage, :, :],
+ accum=(k != 0), dispatch="tcgen05", cta_group=CTA_GROUP)
+ mma2tma.arrive(mma_ps.stage, cta_group=CTA_GROUP, cta_mask=3)
+ mma_ps.advance()
+
+ mma2ld.arrive(0, cta_group=CTA_GROUP, cta_mask=3)
+ tile_scheduler.next_tile()
+
+ # =============================================
+ # Warpgroup 0: Writeback (256 columns in 2 x 128-column chunks)
+ # =============================================
+ elif wg_id == 0:
+ wb_ps = PipelineState(1, phase=0)
+ reg_f16 = T.alloc_local((128,), d_type)
+
+ while tile_scheduler.valid():
+ mma2ld.wait(wb_ps.stage, wb_ps.phase)
+ wb_ps.advance()
+ T.ptx.tcgen05.fence.after_thread_sync()
+
+ for no in T.unroll(2): # 2 chunks of 128 columns = 256 total
+ reg = T.alloc_local((128,), acc_type)
+ reg_wg = reg.view(128, 128,
+ layout=TileLayout(S[(128, 128) : (1@tid_in_wg, 1)]))
+ Tx.wg.copy_async(reg_wg[:], tmem[:, no * 128:(no + 1) * 128])
+ T.ptx.tcgen05.wait.ld()
+ Tx.cast(reg_f16[:], reg[:])
+ Tx.copy(Dsmem[warp_id * 32 + lane_id, :], reg_f16[:])
+ T.ptx.fence.proxy_async("shared::cta")
+ T.cuda.warpgroup_sync(10)
+ if warp_id == 0:
+ if lane_id == 0:
+ n_st_epi = T.meta_var(n_idx * 256 + no * 128)
+ Tx.copy_async(D[m_st:m_st+BLK_M, n_st_epi:n_st_epi+128],
+ Dsmem[:, :], dispatch="tma")
+ T.ptx.cp_async.bulk.commit_group()
+ T.ptx.cp_async.bulk.wait_group(0)
+ T.cuda.warpgroup_sync(10)
+
+ ld2mma.arrive(0, cta_id=0, pred=True)
+ tile_scheduler.next_tile()
+
+ # --- Cleanup ---
+ T.cuda.cluster_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=CTA_GROUP)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=CTA_GROUP)
+
+ return kernel
+```
+
+**2 个 CTA 带来的变化。**
+
+- `CTA_GROUP = 2`,`MMA_N = BLK_N * CTA_GROUP = 256`
+
+- `ld2mma.init(128 * CTA_GROUP)`,两个 CTA 的 writeback WG 都会 arrive
+
+- TMA arrive byte count 包含两个 CTA:`CTA_GROUP * (BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE`
+
+- `tcgen05.alloc` 和 `tcgen05.dealloc` 必须使用 `cta_group=2`
+
+- Writeback 把 256 个 output column 拆成两个 128-column chunk;一次性读取全部 256 个 TMEM column 会超过寄存器容量。Step 9 会进一步把 chunk 缩小到 `EPI_N=64`
+
+- 末尾用 `cluster_sync()` 替代 `cta_sync()`,确保所有 CTA 在 TMEM dealloc 前都完成
+
+这些额外算术强度会直接体现在 wall clock 上:Step 8 在 4096³ 上达到 **0.104 ms**,相对同规模下 70 ms 的 Step-1 算法约 676×(见 End-to-End 表)。Kernel 现在开始偏向 compute-bound,这正好为 Step 9 做铺垫:我们会加入第二个 MMA consumer,让更多 Tensor Core work 保持在飞行中。
+
+如果 Step 8 反而比 Step 7 *更慢*,问题几乎总是某个新的 cluster contract 写错。优先检查三件事:TMA arrive byte count 是否是 `CTA_GROUP * (BLK_M*BLK_K + BLK_N*BLK_K) * F16_SIZE`;scheduler 维度是否是 256×256 cluster tile 对应的 `num_m_tiles=M//256, num_n_tiles=N//256`;writeback 是否发起两次 TMA store,每个 128-column chunk 一次,并且每次都在 Dsmem 复用前 drain。
+
+---
+
+Cluster 提高了 CTA *之间*的复用。最后一步转向内部,在每个 CTA 内通过给 producer 增加第二个 MMA consumer 来提高 compute density。
+
+
(chap_multi_consumer)=
-# 用 Warp Specialization 和 Cluster 扩展 GEMM
+## Step 9:Multi-Consumer Warp Specialization
+
+到 Step 8 时,MMA 已经真正忙起来了,但一个 consumer warp 消费 staged B tile 的速度有限,而这个 B tile 在 SMEM 中一直可用,任何愿意读取它的人都能读。最后一个优化就利用这一点:加入第二个 MMA consumer,让它把*不同* A block 与*同一个* B tile 相乘。每个 CTA 的 compute density 翻倍,cluster output 从 256×256 增长到 512×256。和之前一样,M=N=K=4096。
+
+> **本 step 改变的内容:Scope + Layout**
+> - Scope:一个 MMA consumer 变成两个,由 `warp_id` 选择。
+> - Layout:一个 staged B tile 被两个 consumer 复用;A 增加 consumer axis。
+> - Dispatch:不变。
+
+**主题。**
+
+- 多个 MMA warp(consumer)提高吞吐
+
+- 多个 writeback warpgroup 使用独立 barrier slot
+
+- 本教程中最优化 GEMM 变体使用的结构
+
+
+### Multi-Consumer Structure
+
+加入第二个 consumer 后,kernel 现在需要安排更多不同角色:两个 MMA warp,而不是一个;并配套第二个 writeback warpgroup 来 drain 额外 accumulator。使用 `NUM_CONSUMER=2` 和 `WG_NUMBER=3` 时,kernel 跨三个 warpgroup(角色表中缩写为 WG):
+
+| Warpgroup | Warp | Role |
+|-----------|------|------|
+| **WG 2** | warp 0 | MMA consumer 0:`Asmem[..., 0] x B` -> TMEM cols `[0:256]` |
+| **WG 2** | warp 1 | MMA consumer 1:`Asmem[..., 1] x B` -> TMEM cols `[256:512]` |
+| **WG 2** | warp 3 | TMA producer:每个 stage 加载 2x A block + 1x B block |
+| **WG 0** | all | consumer 0 的 writeback:读取 TMEM `[0:256]` |
+| **WG 1** | all | consumer 1 的 writeback:读取 TMEM `[256:512]` |
+
+整个安排依赖一个不对称性。每个 consumer 都把自己的 A block 与*同一个* staged B tile 相乘,因此单次 B load 现在喂给 2× MMA work,B 的每有效 FLOP load cost 实际减半。我们共享 B 而不是 A 的原因是,两个 consumer 覆盖不同 M-row stripe:它们的 A block 真实不同,而 B 对二者相同。练习 3 会要求你说服自己,这是唯一可行的共享。
+
+### 相对 Step 8 的变化
+
+具体来说,支持第二个 consumer 会触碰 kernel 中少数几处,而每个变化都可以追溯到同一个事实:现在每个 stage 有两个 A block 和两个 TMEM range 需要喂给并 drain,而 B 保持共享。下面的修改会 stage 额外 A block,给每个 consumer 自己的 barrier slot,并为更高的 512×256 cluster tile 调整 tile addressing。
+
+- `Asmem = pool.alloc((PIPE_DEPTH, NUM_CONSUMER, BLK_M, BLK_K), ...)`:每个 stage 两个 A block,每个 consumer 一个
+
+- TMA 同时加载 `Asmem[stage, 0]` 和 `Asmem[stage, 1]`,TMA arrive byte 现在是 `CTA_GROUP * (NUM_CONSUMER * BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE`(额外 A block)
+
+- MMA warp `warp_id` 选择哪个 A block 和 TMEM range
+
+- `mma2tma.init(NUM_CONSUMER)`:每个 stage 两个 consumer 都要向 TMA signal
+
+- `mma2ld` 和 `ld2mma` 的 `depth=NUM_CONSUMER`:每个 consumer 使用自己的 barrier slot(MMA 侧用 `warp_id`,writeback 侧用 `wg_id`)
+
+- Tile address:`m_st = (m_idx * NUM_CONSUMER * CTA_GROUP + cbx) * BLK_M`。M 方向有额外 `NUM_CONSUMER` 因子,因为每个 cluster tile 现在沿 M 跨越 `NUM_CONSUMER` 个 consumer。Tile scheduler 使用 `num_m_tiles = M // 256 // NUM_CONSUMER`(cluster tile 是 512x256)
+
+- Writeback 使用 chunked `EPI_N`,让每次 iteration 中 live 的 TMEM-readback register 更少
+
+
+**实现。**
+
+```python
+def hgemm_v9(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ CTA_GROUP = 2
+ NUM_CONSUMER = 2
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ MMA_N = BLK_N * CTA_GROUP # 256
+ K_TILES = K // BLK_K
+ PIPE_DEPTH = 4
+ EPI_N = 64
+ WG_NUMBER = 3
+ F16_SIZE = 2 # fp16
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (PIPE_DEPTH, NUM_CONSUMER, BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (PIPE_DEPTH, BLK_N, BLK_K))
+ D_layout = tma_shared_layout(d_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (NUM_CONSUMER, BLK_M, EPI_N))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ bx = T.cta_id([SM_COUNT])
+ cbx, cby = T.cta_id_in_cluster([CTA_GROUP, 1])
+ wg_id = T.warpgroup_id([WG_NUMBER])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- Allocation ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ tma2mma = TMABar(pool, PIPE_DEPTH)
+ mma2tma = TCGen05Bar(pool, PIPE_DEPTH)
+ mma2ld = TCGen05Bar(pool, NUM_CONSUMER) # depth=2, one slot per consumer
+ ld2mma = MBarrier(pool, NUM_CONSUMER) # depth=2, one slot per consumer
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((PIPE_DEPTH, NUM_CONSUMER, BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((PIPE_DEPTH, BLK_N, BLK_K), b_type, layout=B_layout)
+ Dsmem = pool.alloc((NUM_CONSUMER, BLK_M, EPI_N), d_type, layout=D_layout)
+
+ # --- Barrier init ---
+ tma2mma.init(1)
+ mma2tma.init(NUM_CONSUMER) # each stage expects 2 arrivals
+ mma2ld.init(1) # each slot gets 1 arrival
+ ld2mma.init(128 * CTA_GROUP) # both CTAs' writeback threads
+ pool.commit()
+
+ # --- TMEM alloc (cooperative) ---
+ if wg_id == 0:
+ if warp_id == 0:
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=CTA_GROUP)
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), acc_type, scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)]))
+
+ # --- Tile scheduler (512x256 cluster tiles) ---
+ tile_scheduler = ClusterPersistentScheduler2D(
+ "ts", num_m_tiles=M // 256 // NUM_CONSUMER, num_n_tiles=N // 256,
+ l2_group_size=8, num_clusters=SM_COUNT // CTA_GROUP)
+ tile_scheduler.init(bx // CTA_GROUP)
+ m_idx = T.meta_var(tile_scheduler.m_idx)
+ n_idx = T.meta_var(tile_scheduler.n_idx)
+ m_st = T.meta_var((m_idx * NUM_CONSUMER * CTA_GROUP + cbx) * BLK_M)
+ n_st = T.meta_var((n_idx * CTA_GROUP + cbx) * BLK_N)
+
+ tma2mma_cta0 = tma2mma.remote_view(0)
+
+ # =============================================
+ # Warpgroup 2: TMA Producer (warp 3) + 2 MMA Consumers (warp 0, 1)
+ # =============================================
+ if wg_id == 2:
+ if warp_id == 3:
+ # === TMA Producer: loads 2 A blocks + 1 B block per stage ===
+ tma_ps = PipelineState(PIPE_DEPTH, phase=1)
+
+ @T.inline
+ def tma_load(k_offset):
+ m_st_c1 = T.meta_var(m_st + CTA_GROUP * BLK_M)
+ Tx.copy_async(Asmem[tma_ps.stage, 0, :, :],
+ A[m_st:m_st+BLK_M, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=CTA_GROUP,
+ mbar=tma2mma_cta0.ptr_to([tma_ps.stage]))
+ Tx.copy_async(Asmem[tma_ps.stage, 1, :, :],
+ A[m_st_c1:m_st_c1+BLK_M, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=CTA_GROUP,
+ mbar=tma2mma_cta0.ptr_to([tma_ps.stage]))
+ Tx.copy_async(Bsmem[tma_ps.stage, :, :],
+ B[n_st:n_st+BLK_N, k_offset:k_offset+BLK_K],
+ dispatch="tma", cta_group=CTA_GROUP,
+ mbar=tma2mma_cta0.ptr_to([tma_ps.stage]))
+
+ if T.filter(lane_id, T.ptx.elect_sync()):
+ while tile_scheduler.valid():
+ for k in range(K_TILES):
+ mma2tma.wait(tma_ps.stage, tma_ps.phase)
+ tma_load(k * BLK_K)
+ if cbx == 0:
+ tma2mma_cta0.arrive(tma_ps.stage,
+ CTA_GROUP * (NUM_CONSUMER * BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE)
+ tma_ps.advance()
+ tile_scheduler.next_tile()
+
+ elif warp_id < NUM_CONSUMER:
+ # === MMA Consumer: warp_id selects A block and TMEM range ===
+ mma_ps = PipelineState(PIPE_DEPTH, phase=0)
+ ld_ps = PipelineState(1, phase=1)
+
+ if cbx == 0:
+ if T.filter(lane_id, T.ptx.elect_sync()):
+ while tile_scheduler.valid():
+ ld2mma.wait(warp_id, ld_ps.phase)
+ ld_ps.advance()
+
+ for k in range(K_TILES):
+ tma2mma.wait(mma_ps.stage, mma_ps.phase)
+ Tx.gemm_async(
+ tmem[:, warp_id * MMA_N:warp_id * MMA_N + MMA_N],
+ Asmem[mma_ps.stage, warp_id, :, :],
+ Bsmem[mma_ps.stage, :, :],
+ accum=(k != 0), dispatch="tcgen05", cta_group=CTA_GROUP)
+ mma2tma.arrive(mma_ps.stage, cta_group=CTA_GROUP, cta_mask=3)
+ mma_ps.advance()
+
+ mma2ld.arrive(warp_id, cta_group=CTA_GROUP, cta_mask=3)
+ tile_scheduler.next_tile()
+
+ # =============================================
+ # Warpgroup 0/1: Writeback (each reads its consumer's TMEM range)
+ # =============================================
+ elif wg_id < NUM_CONSUMER:
+ wb_ps = PipelineState(1, phase=0)
+ reg_f16 = T.alloc_local((EPI_N,), d_type)
+
+ while tile_scheduler.valid():
+ mma2ld.wait(wg_id, wb_ps.phase) # wait for THIS consumer
+ wb_ps.advance()
+ T.ptx.tcgen05.fence.after_thread_sync()
+
+ # Read TMEM in EPI_N=64 column chunks (4 iterations for 256 cols)
+ for i in T.unroll(MMA_N // EPI_N):
+ reg = T.alloc_local((EPI_N,), acc_type)
+ reg_wg = reg.view(128, EPI_N,
+ layout=TileLayout(S[(128, EPI_N) : (1@tid_in_wg, 1)]))
+ col_st = T.meta_var(wg_id * MMA_N + i * EPI_N)
+ col_end = T.meta_var(wg_id * MMA_N + i * EPI_N + EPI_N)
+ Tx.wg.copy_async(reg_wg[:], tmem[:, col_st:col_end])
+ T.ptx.tcgen05.wait.ld()
+ Tx.cast(reg_f16[:], reg[:])
+ Tx.copy(Dsmem[wg_id, warp_id * 32 + lane_id, :], reg_f16[:])
+ T.ptx.fence.proxy_async("shared::cta")
+ T.cuda.warpgroup_sync(wg_id + 10)
+ if warp_id == 0:
+ if lane_id == 0:
+ m_st_epi = T.meta_var(
+ (m_idx * NUM_CONSUMER * CTA_GROUP + wg_id * CTA_GROUP + cbx) * BLK_M)
+ n_st_epi = T.meta_var(n_idx * MMA_N + i * EPI_N)
+ Tx.copy_async(
+ D[m_st_epi:m_st_epi+BLK_M, n_st_epi:n_st_epi+EPI_N],
+ Dsmem[wg_id, :, :], dispatch="tma")
+ T.ptx.cp_async.bulk.commit_group()
+ T.ptx.cp_async.bulk.wait_group(0)
+ T.cuda.warpgroup_sync(wg_id + 10)
+
+ ld2mma.arrive(wg_id, cta_id=0, pred=True)
+ tile_scheduler.next_tile()
+
+ # --- Cleanup ---
+ T.cuda.cluster_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=CTA_GROUP)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=CTA_GROUP)
+
+ return kernel
+```
+
+**实现备注。**
+
+- 在 Step 9 设计中,`mma2ld` 和 `ld2mma` 都是一个 `depth=NUM_CONSUMER` 的共享对象,而不是每个 consumer 一个对象。Slot 0 连接 MMA warp 0 和 Warpgroup 0,slot 1 连接 MMA warp 1 和 Warpgroup 1;MMA 侧用 `warp_id` 索引,writeback 侧用 `wg_id` 索引。
+
+## End-to-End Result
+
+下表报告了从 naive baseline 到 warp-specialized cluster kernel 的实测里程碑,并列出 cuBLAS reference。参考数字来自 NVIDIA B200,M=N=K=4096,fp16,locked clocks,1000-iteration timed benchmark:
+
+| Step | Technique | Time | Speedup |
+|------|-----------|------|---------|
+| 1 | Sync load + MMA | 70 ms | 1× |
+| 2 | K-loop accumulation | --- | Handle K larger than one tile |
+| 3 | Spatial tiling | 53.6 ms | ~1.3× |
+| 4 | TMA async load | 0.49 ms | ~142× |
+| 5 | Software pipeline | --- | Overlap load + compute |
+| 6 | Persistent kernel | --- | L2 cache locality |
+| 7 | Warp specialization | 0.23 ms | ~309× |
+| 8 | 2-CTA cluster | 0.104 ms | ~676× |
+| 9 | Multi-consumer | 0.094 ms | ~744× |
+| --- | cuBLAS (reference) | 0.094 ms | ~744× |
+
+这张表中的所有时间,包括 70 ms 的 Step 1 baseline,都在同一个 M=N=K=4096 规模下测得,因此 speedup chain 可以端到端比较。这里需要精确说明 70 ms 到底是什么,因为很容易误读。它*不是* {ref}`chap_gemm_basics` 中的 single-tile Step-1 kernel 在 4096³ 上运行;那个 kernel 只会计算一个 128×128 tile,并且只在小规模上运行。70 ms 指的是一个 naive full-size baseline,它采用同样的 sequential、single-tile 思路,并扩展到完整 4096³ 问题。Steps 1-3 在 {ref}`chap_gemm_basics` 中以小规模(128×128 和 256³)介绍,是为了让最初的 walkthrough 简单;这里 Step 1 和 Step 3 行是它们的 full-size benchmark counterpart。剩下的 dash(Steps 2、5、6)表示这些 step 用于展示结构,但没有单独计时。
+
+请把这些数字看作受控条件下的一次 B200 reference run,而不是 leaderboard。每个 step 中嵌入的 `{.python .input}` benchmark cell 是 smoke benchmark:适合观察趋势,不适合声明峰值性能。
+
+几乎所有收益都来自四项技术:
+
+1. **TMA Async Data Movement**:硬件 copy engine 替代 software copy(Step 1 -> Step 4 约 142×)。要正确理解这个 142×:它反映的是从单个 128×128-tile kernel(grid 1×1)一路变成带 K-loop、spatial tiling、多 CTA 和 TMA 的 full tiled-and-parallel kernel;这不是 TMA 单独贡献。要隔离 TMA,需要比较两个只在 copy mechanism 上不同的 full-size kernel。
+2. **Software Pipelining + Warp Specialization**:通过给 load 和 compute 各自专门角色来重叠二者(Step 4 -> Step 7 约 2.2×)。
+3. **CTA Clusters**:2-SM cooperative MMA 提高 CTA 间的 B-tile 复用(本 benchmark 中 Step 7 -> Step 8 约 2.2×)。
+4. **Multi-Consumer**:两个 MMA warp 提高 compute density(Step 8 -> Step 9 约 10%)。
+
+把这些 measured milestone 画出来,同样四项贡献会描出从同步 tiled kernel 走向 cuBLAS reference 的下降曲线。下图展示选中的 measured point:
+
+
+
+注意,越往后 gain 越小,这背后有结构性原因,而不是优化力度下降。早期 step 攻击的是*内存*瓶颈(TMA 替代 software copy,cluster 提高 arithmetic intensity),而 70 ms 中大部分时间确实花在这里,所以这些 step 收益最大。到 Step 8,kernel 已经在 cuBLAS 的约 10% 以内(0.104 vs 0.094 ms),并接近 *compute-bound*,这意味着几乎没有多少 memory stall 可隐藏;Step 9 的 multi-consumer overlap 收回了剩下的大部分空间。接近 compute ceiling 时,最后约 10% gain 正是预期中的 diminishing return:问题几乎已经解决,而不是优化本身薄弱。
+
+本章构建的所有内容(TMA load、`tcgen05` MMA、TMEM readback 和 warp-specialized barrier)都会直接带到下一章。Flash Attention 会复用所有这些能力,然后通过在两个 MMA phase 之间插入 online-softmax step,而不是简单重复同一个 MMA,进一步提高难度。
+
+
+## 练习
-> 翻译状态:待翻译。对应英文章节:`chapter_gemm_advanced/index.md`。
+1. 如果在 Step 7 中把 TMA 和 MMA `PipelineState` 的初始 `phase` 都设为 `0`,会发生什么?画出 deadlock 场景。
+2. Step 8 使用 `cta_group=2` 时,TMA arrive byte count 是 `CTA_GROUP * (BLK_M*BLK_K + BLK_N*BLK_K) * F16_SIZE`。既然每个 CTA 加载自己的数据,为什么还要乘以 `CTA_GROUP`?
+3. Step 9 中,每个 consumer 处理不同 M row,但使用同一个 B tile。为什么共享 B 而不是 A 是正确选择?
-本页用于放置 warp specialization、persistent scheduling 和 cluster GEMM 相关内容的中文翻译。
+**Try with your agent**:粘贴 Step 7 kernel,让它 trace 一个 K-tile 穿过四个 barrier(`tma2mma`、`mma2tma`、`mma2ld`、`ld2mma`)。对每个 barrier,问清楚谁 wait、谁 arrive、哪个 tile 变得可读,以及哪个 buffer 随后变得可复用。
diff --git a/zh/chapter_gemm_async/index.md b/zh/chapter_gemm_async/index.md
index 43dc3fe2..d7aca284 100644
--- a/zh/chapter_gemm_async/index.md
+++ b/zh/chapter_gemm_async/index.md
@@ -1,13 +1,686 @@
----
-orphan: true
----
-
(chap_gemm_async)=
+# 用 TMA 为 GEMM 建立 Pipeline
+
+:::{admonition} 概览
+:class: overview
+
+- 基础 GEMM 在两个本可同时运行的阶段之间轮流执行:copy 一个 tile、compute、再 copy 下一个 tile,因此浪费了大量时间。
+- Step 4 切换到 TMA async load,Step 5 对 SMEM 做 double buffering 并 prefetch(`PIPE_DEPTH=2`);完整 load/compute overlap 要等到 Step 7 的 warp specialization,Step 6 则通过 tile scheduler 把 kernel 变成 persistent kernel。
+- 目标是在 Tensor Core 计算当前 tile 的同时,加载下一个 tile。
+:::
+
+Tensor Core 是芯片上最昂贵的单元,而上一章正确的 tiled GEMM 让它在大部分时钟周期里空闲。Kernel 轮流执行:线程把一个 tile copy 到 shared memory,Tensor Core 消费它,线程再 copy 下一个 tile,Tensor Core 等待。每个阶段都卡在前一个阶段之后,尽管加载下一个 tile 和计算当前 tile 使用的是完全不同的硬件,本可以同时运行。要缩小这个差距,不需要新数据路径;tile、layout 和数学都已经正确。需要改变的是 work *何时*发生,以及由*谁*调度。本章保持 tile 数据路径完全不变,直接攻击空闲时间。
+
+我们通过三个递进 step 到达那里。先知道终点会有帮助。Step 4 把 bulk GMEM <-> SMEM transfer 交给 TMA,让专用 copy 硬件移动 tile,而不是由线程移动。Step 5 加入两级 software pipeline,让下一个 K tile 在当前 tile 仍被乘法消费时有地方落下。Step 6 把 launch 重塑为由 tile scheduler 驱动的 persistent kernel,摊销 per-tile setup,并允许我们选择能让 operand 保持 hot 的 tile order。整个过程中,SMEM、TMEM 和 register layout 都保持上一章留下的样子。真正的新思想只有硬件单元之间的异步交接:让一个引擎跑在另一个前面,而不是让它们 lockstep 前进。
+
(chap_tma_async)=
+## Step 4:TMA Async Load
+
+第一步是把 copy 本身移出关键路径。回想 Steps 1-3 中 CTA 在做什么:每个线程都计算地址并发出 load 指令,目的只是把 tile 搬进 SMEM。这些指令带宽花在了 plumbing 上,而不是数学计算上。Step 4 用 TMA 替换同步 `Tx.copy`:一个线程发出一条命令,TMA engine 自己完成整个 tile transfer。从这里开始,示例使用完整 M=N=K=4096 规模,而不再使用 Steps 1-3 中的小规模;它们的 end-to-end timing 会出现在 {ref}`chap_gemm_advanced` 末尾的 *End-to-End Result* 表中。
+
+> **本 step 改变的内容:Dispatch**
+> - Scope:不变,一个 warpgroup。
+> - Layout:不变,同样的 SMEM/TMEM/register tile。
+> - Dispatch:GMEM -> SMEM load 从同步 `Tx.copy` 切换到 TMA engine。
+
+### TMA 发起模式
+
+Step 4 的唯一变化是把同步 tile copy 换成 TMA load,因此值得仔细看这个 load 如何发起。源码修改只有几行,但这些行背后的执行模型完全不同。同步 `Tx.copy` 是 CTA 线程自己用自己的指令完成的 work;TMA copy 是一个线程发出的命令,之后所有移动都由 TMA 硬件完成。把两者并排看最清楚。
+
+**之前(Step 3)**:全部 128 个线程参与 copy,然后 `cta_sync` 让 shared-memory write 可见:
+
+```python
+Tx.cta.copy(Asmem[:, :], A[m_st:m_st+BLK_M, i*BLK_K:(i+1)*BLK_K]) # all 128 threads
+Tx.cta.copy(Bsmem[:, :], B[n_st:n_st+BLK_N, i*BLK_K:(i+1)*BLK_K])
+T.cuda.cta_sync()
+```
+
+**之后(Step 4)**:一个线程发起 TMA load,mbarrier 跟踪硬件 transfer 何时完成:
+
+```python
+tid = warp_id * 32 + lane_id # 0..127 within the warpgroup
+if tid == 0: # exactly one thread starts TMA
+ Tx.copy_async(Asmem, A[...], dispatch="tma")
+ Tx.copy_async(Bsmem, B[...], dispatch="tma")
+ T.ptx.mbarrier.arrive.expect_tx(tma_bar, byte_count) # bytes expected from TMA
+T.ptx.mbarrier.try_wait(tma_bar, phase) # wait before MMA reads SMEM
+```
+
+注意,load 用 `tid == 0` gate,而不是用 `elect_sync()`;这个差异比看起来更重要。`elect.sync` 会在*每个 warp* 中选择一个 active lane,而一个 warpgroup 有四个 warp,因此 `elect_sync()` 实际会让四个线程进入 load protocol。问题在于,这个 protocol 会向 mbarrier 宣告 expected byte count,而且必须只宣告一次;四次宣告会破坏计数,让 wait 无法正确 release。用 warpgroup-wide id 精确选择一个线程,是避免这个问题的干净方法。
+
+也要诚实说明 speedup 来自哪里。Step 4 仍然在每次 TMA load 后等待,所以还没有让 load 与 compute 重叠;那是 Step 5 的工作。这里的收益纯粹来自 data-movement path 的改变:
+
+- `Tx.copy` 使用 CTA 线程计算地址,并发出 load/store 指令。
+- TMA 使用一条发起命令启动硬件 tile transfer。地址生成、coalescing 和 swizzling 由 TMA descriptor 描述,并由 TMA engine 执行。
+
+因此,即使 Step 4 仍然阻塞在每次 load 上,它也会更快。TMA 吸收了 bulk transfer,让 CTA 线程不必花指令带宽搬 tile;仅这一点就足以改善性能。
+
+### TMA Load 和 Store 同步
+
+我们已经看过 TMA copy 如何发起;故事的另一半是如何知道它已经完成。切换到 TMA 会同时改变两件事:谁开始 copy,以及代码如何知道它完成。第一件事在代码中很明显;第二件事很容易忽略,而一旦搞错,得到的是静默 correctness bug,不是 crash。使用 `Tx.cta.copy` 时,CTA 线程共同完成 copy,后面的 `cta_sync()` 足以说明它完成。使用 TMA 时,一个被选中的线程发起 `Tx.copy_async(..., dispatch="tma")`,engine 按自己的 schedule 执行 transfer,并通过 mbarrier 发出完成信号。
+
+这正是为什么 `cta_sync()` 不再足够。`cta_sync()` 只等待 CTA 自己的线程,并且只排序这些线程的 shared-memory write;它对正在飞行的 TMA transfer 一无所知,因此可能在 tile 仍在到达时就返回。修复方法是显式表达完成:对于 TMA load,被选中的线程先告诉 mbarrier 要期待多少 byte,然后 CTA 在任何 MMA 触碰 SMEM tile 之前等待*这个* mbarrier。下图完整追踪了这个 handshake。
+
+
+
+上图隔离了 load 侧的 handshake:一个选中的线程 launch TMA,mbarrier 计数 expected byte,MMA 在读取 SMEM 之前等待 release。图中 “Elected Thread” 指的是发起 TMA 的 selected thread,在我们的代码里就是 `tid == 0` 线程,而不是 `elect_sync()` 选出的 lane。
+
+把 load 路径合起来看:selected thread 发起两个 `copy_async` 调用,然后执行 `arrive.expect_tx(total_bytes)`,其中 byte count 精确说明 mbarrier 应该等待多少数据。Engine 移动完这些 byte 后,匹配的 `mbarrier.try_wait(phase)` 才 release,只有这时 SMEM tile 才能安全喂给 MMA。
+
+Store 侧经过同一套硬件,但等待方式不同,所以最好在脑中清楚区分两个 protocol:load 用 mbarrier 和 byte count 跟踪完成,而 store 用 commit group 和 wait group 跟踪完成。线程把 fp16 结果写入 `Dsmem` 并同步后,一个 selected thread 发起 `Tx.copy_async(D[...], Dsmem, dispatch="tma")`,然后 `cp_async.bulk.commit_group()` 加 `cp_async.bulk.wait_group(0)` 会阻塞直到 store drain。这个 wait 不是可选的:上一轮 store 完成前,`Dsmem` 不能被下一个 tile 复用。
+
+**Try with your agent**:Trace Step 4 中一个 K tile 的 load 和 store 同步。指出哪个线程启动每条 TMA 命令,哪个 mbarrier 或 commit group 跟踪完成,哪个 wait 保护 MMA 对 `Asmem` 和 `Bsmem` 的读取,哪个 wait 保护 `Dsmem` 的复用。为什么这里用 `elect_sync()` 选择 TMA load 线程是错误的?
+
+### 完整 Kernel
+
+完整 kernel 把 TMA load 和 store 合入 Step 3 的结构,其余结构保持不变。Import 与之前相同:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+```
+
+它包在 `hgemm_v4(M, N, K)` 中,这是本书一直采用的模式:wrapper 把 shape-dependent constant 和 layout 放在使用它们的 kernel 旁边。
+
+```python
+def hgemm_v4(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ K_TILES = K // BLK_K
+ F16_SIZE = 2
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_N, BLK_K))
+ D_layout = tma_shared_layout(d_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_N))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ bx, by = T.cta_id([M // BLK_M, N // BLK_N])
+ wg_id = T.warpgroup_id([1])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- SMEM allocation (now includes Dsmem for TMA store) ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ tma_bar = pool.alloc((1,), "uint64", align=8)
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((BLK_N, BLK_K), b_type, layout=B_layout)
+ Dsmem = pool.alloc((BLK_M, BLK_N), d_type, layout=D_layout)
+ pool.commit()
+
+ # --- Barrier + TMEM init ---
+ if warp_id == 0 and lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ T.ptx.mbarrier.init(tma_bar.ptr_to([0]), 1)
+ if warp_id == 0:
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), "float32", scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)])
+ )
+
+ m_st = T.meta_var(bx * BLK_M)
+ n_st = T.meta_var(by * BLK_N)
+ phase_tma: T.int32 = 0
+ phase_mma: T.int32 = 0
+
+ # --- Inline helpers ---
+ @T.inline
+ def tma_load(k_st):
+ tma_config = T.meta_var({
+ "dispatch": "tma", "cta_group": 1,
+ "mbar": tma_bar.ptr_to([0])
+ })
+ Tx.copy_async(Asmem[:, :],
+ A[m_st : m_st + BLK_M, k_st : k_st + BLK_K],
+ **tma_config)
+ Tx.copy_async(Bsmem[:, :],
+ B[n_st : n_st + BLK_N, k_st : k_st + BLK_K],
+ **tma_config)
+ T.ptx.mbarrier.arrive.expect_tx(
+ tma_bar.ptr_to([0]),
+ (BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE
+ )
+
+ @T.inline
+ def mma(accum):
+ Tx.gemm_async(
+ tmem[:, :BLK_N], Asmem[:, :], Bsmem[:, :],
+ accum=accum, dispatch="tcgen05", cta_group=1
+ )
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ # --- K-loop with TMA async ---
+ tid = T.meta_var(warp_id * 32 + lane_id)
+ for k in range(K_TILES):
+ k_st = T.meta_var(k * BLK_K)
+
+ # Single thread issues TMA load
+ if tid == 0:
+ tma_load(k_st)
+
+ # Wait for TMA to finish; the mbarrier release carries SMEM
+ # visibility to the subsequent MMA, so no extra fence is needed.
+ T.ptx.mbarrier.try_wait(tma_bar.ptr_to([0]), phase_tma)
+
+ # Single thread issues MMA
+ if tid == 0:
+ mma(accum=k != 0)
+
+ # Wait for MMA to finish
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+ phase_tma ^= 1
+ phase_mma ^= 1
+
+ # --- TMA Store Writeback ---
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+
+ # Read TMEM -> registers (async; wait.ld then cta_sync to ensure read completes)
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+ T.cuda.cta_sync()
+ # Cast fp32 -> fp16
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ # Write registers -> Dsmem, flush, then sync
+ Tx.copy(Dsmem[warp_id * 32 + lane_id, 0:BLK_N], Dreg_f16[:])
+ T.ptx.fence.proxy_async("shared::cta")
+ T.cuda.warpgroup_sync(10)
+ # TMA store: Dsmem -> GMEM. One selected thread starts the store and drains the
+ # store group before Dsmem is reused.
+ if tid == 0:
+ Tx.copy_async(D[m_st : m_st + BLK_M, n_st : n_st + BLK_N],
+ Dsmem[:, :], dispatch="tma")
+ T.ptx.cp_async.bulk.commit_group()
+ T.ptx.cp_async.bulk.wait_group(0)
+ T.cuda.warpgroup_sync(10)
+
+ # --- Deallocate TMEM ---
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
+
+### Kernel 中的 TMA 配置
+
+这个 kernel 里的大部分内容都继承自 Step 3。真正携带 TMA 语义的配置点只有五个,值得逐一认识:
+
+- **TMA config**:`{"dispatch": "tma", "cta_group": 1, "mbar": tma_bar.ptr_to([0])}` 告诉 `Tx.copy_async` 使用 TMA,并通过 `tma_bar` 报告 load completion。
+
+- **Byte count**:`(BLK_M * BLK_K + BLK_N * BLK_K) * 2` 是两个 fp16 operand tile 加载的 byte 数。`arrive.expect_tx(...)` 把这个 count 交给 mbarrier。
+
+- **mbarrier initialization**:`init(tma_bar.ptr_to([0]), 1)` 创建 TMA load 使用的 completion barrier。
+
+- **`@T.inline`**:`tma_load(...)` 和 `mma(...)` 是 helper function。它们在编译时展开到 kernel body 中,并且可以使用外围 kernel 的变量。
+
+- **TMA store synchronization**:Epilogue 先把 fp16 row 写入 `Dsmem`。`fence.proxy_async` 和 `warpgroup_sync` 让这些由线程写入的 SMEM 值准备好供 TMA store path 使用。Store 随后使用 `commit_group()` 和 `wait_group(0)` 等待 SMEM-to-GMEM transfer 完成。
+
+现在我们有了正确的组件,但节奏仍然不对。Step 4 仍然会在开始匹配 MMA 前完成每次 load,所以 load 和 multiply 从未真正同时运行;我们努力分开的两个引擎仍然在轮流工作。下一步保持 TMA load 和 store 路径完全不变,只重新安排 schedule,让一个 K tile 的加载可以在另一个 tile 上的 compute 运行时进行。
+
(chap_software_pipeline)=
+## Step 5:Software Pipeline(PIPE_DEPTH=2)
+
+既然 load 和 compute 明显是独立引擎,为什么 Step 4 仍然无法重叠它们?障碍其实是 storage。只有一对 SMEM tile 时,下一个 load 没地方落:它必须等当前 MMA 读完这对 tile 后才能开始,否则会覆盖仍在使用的数据。Step 5 通过 double-buffering shared memory 消除这个 storage conflict。Single-warpgroup loop 仍然会在 launch 下一次 TMA load 之前等待每次 MMA,但现在有不同 stage 可以 prefetch 和复用。我们仍然使用完整 M=N=K=4096 规模。
+
+> **本 step 改变的内容:Layout**
+> - Scope:不变,一个 warpgroup。
+> - Layout:单个 SMEM tile pair 变成 `PIPE_DEPTH` stage 的 ring buffer。
+> - Dispatch:不变,仍然是 TMA load 和 `tcgen05` MMA;这个 step 加入 prefetch 和 stage reuse,完整 load/compute overlap 会在 Step 7 到来。
+
+### Pipeline Walkthrough
+
+当 `PIPE_DEPTH=2` 时,kernel 会分配两个 SMEM stage,让 load 路径和 MMA 路径有不同 slot 可以工作。
+
+请把下图理解成两级 buffer 想要支持的 pipeline structure,而不是这个 single-warpgroup kernel 的精确执行 trace。Step 5 构建 ring buffer 并 prefetch 后续 stage,但主循环仍然在发起下一次 TMA load 前等待当前 MMA。完整 load/compute overlap 会在 Step 7 到来,那时 warp specialization 会把 TMA 和 MMA 分成不同角色。
+
+
+
+Primed 之后,loop 会在两个 stage 间交替。最开始两次 TMA load 会填充两个 stage;之后,loop 等待当前 stage、在其上运行 MMA、等待该 MMA 读完这个 stage,然后把 `k + PIPE_DEPTH` 的 load 发到刚刚变得可复用的 stage 中。这还不是并发 TMA/MMA schedule,但它建立了 Step 7 会拆分到 producer 和 consumer role 中的 ring-buffer 结构。
+
+具体来说,代码与 Step 4 有四处不同:
+
+1. `Asmem` 和 `Bsmem` 增加一个前导 `PIPE_DEPTH` 维度,因此每个 stage 都有自己的 SMEM storage。
+2. `tma_bar` 变成一个 array,每个 stage 一个 mbarrier。
+3. 主 K loop 之前,kernel prefetch 前两个 stage。
+4. K loop 使用 `stage = k % PIPE_DEPTH`:等待当前 stage,在其上运行 MMA,然后把这个 stage 复用于 `k + PIPE_DEPTH`。
+
+### Pipeline 机制
+
+**1. Prefetch**:主循环开始前,我们加载前 `PIPE_DEPTH` 个 stage,因此 loop 第一次 iteration 就能发现数据已经在等它:
+
+```python
+for s in range(min(PIPE_DEPTH, K_TILES)):
+ tma_load(s, s * BLK_K)
+```
+
+**2. Main loop**:对每个 K tile,等待对应 stage ready,在其上运行 MMA,然后立刻把这个刚释放的 stage 重新投入工作,发起领先 `PIPE_DEPTH` 的 tile load:
+
+```python
+stage = k % PIPE_DEPTH
+wait(tma_bar[stage], phase_tma)
+mma(stage, accum)
+wait(mma_bar[0], phase_mma)
+phase_mma ^= 1
+tma_load(stage, next_k * BLK_K)
+```
+
+**3. Phase management**:这是容易绊倒人的地方,但规则比第一眼看上去简单。每个 barrier 的 phase-flip 规则直接来自这个 barrier 有多少个 slot,因此两个 barrier 的翻转节奏不同。MMA accumulator 位于一个 TMEM slot 中,所以 `mma_bar` 是单个 barrier(`mma_bar.ptr_to([0])`),每次 iteration 都会再次访问;每次 iteration 都会访问的 barrier,每次 iteration 都必须 flip phase。TMA barrier 的故事不同:它们形成一个长度为 `PIPE_DEPTH` 的 array,每个 stage 一个 barrier;某个具体 stage 的 barrier 只有在 ring 转一圈后才会再次被访问。因此 `phase_tma` 只在 stage index wrap 回 0 前后,也就是当前 stage 是最后一个 stage 时翻转:
+
+```python
+if stage == PIPE_DEPTH - 1:
+ phase_tma ^= 1
+```
+
+**Try with your agent**:设 `PIPE_DEPTH=2` 且 `K_TILES=5`,让它 trace 主循环。对每个 `k`,列出 `stage`、传给 wait 的 `phase_tma` 和 `phase_mma` 值,以及是否发起新的 prefetch。`phase_tma` 精确在哪里 flip?为什么最后两个 iteration 没有 prefetch?
+
+### 完整 Kernel
+
+完整 kernel 保持 Step 4 的 TMA load 和 store 路径原样,然后把它包进刚才描述的 staged buffer 和 phase 逻辑中。Import 不变:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+```
+
+它包在 `hgemm_v5(M, N, K)` 中。常量 `PIPE_DEPTH=2` 设置 pipeline stage 数量(这里是两个,正好是 double buffering):
+
+```python
+PIPE_DEPTH = 2
+
+def hgemm_v5(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+ F16_SIZE = 2
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ K_TILES = K // BLK_K
+
+ # Double-buffered layouts: first dimension is pipeline stage
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (PIPE_DEPTH, BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (PIPE_DEPTH, BLK_N, BLK_K))
+ D_layout = tma_shared_layout(d_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (BLK_M, BLK_N))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ bx, by = T.cta_id([M // BLK_M, N // BLK_N])
+ wg_id = T.warpgroup_id([1])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- SMEM allocation ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ # Double-buffered TMA barriers (one per stage), single MMA barrier
+ tma_bar = pool.alloc((PIPE_DEPTH,), "uint64", align=8)
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((PIPE_DEPTH, BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((PIPE_DEPTH, BLK_N, BLK_K), b_type, layout=B_layout)
+ Dsmem = pool.alloc((BLK_M, BLK_N), d_type, layout=D_layout)
+ pool.commit()
+
+ # Initialize barriers: PIPE_DEPTH for TMA, 1 for MMA
+ if warp_id == 0:
+ if lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ for s in range(PIPE_DEPTH):
+ T.ptx.mbarrier.init(tma_bar.ptr_to([s]), 1)
+ if warp_id == 0:
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), acc_type, scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)])
+ )
+
+ m_st = T.meta_var(bx * BLK_M)
+ n_st = T.meta_var(by * BLK_N)
+ phase_tma: T.int32 = 0
+ phase_mma: T.int32 = 0
+
+ @T.inline
+ def tma_load(stage, k_offset):
+ tma_config = T.meta_var({
+ "dispatch": "tma", "cta_group": 1,
+ "mbar": tma_bar.ptr_to([stage])
+ })
+ Tx.copy_async(Asmem[stage, :, :],
+ A[m_st:m_st+BLK_M, k_offset:k_offset+BLK_K],
+ **tma_config)
+ Tx.copy_async(Bsmem[stage, :, :],
+ B[n_st:n_st+BLK_N, k_offset:k_offset+BLK_K],
+ **tma_config)
+ T.ptx.mbarrier.arrive.expect_tx(
+ tma_bar.ptr_to([stage]),
+ (BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE)
+
+ @T.inline
+ def mma(stage, accum):
+ Tx.gemm_async(tmem[:, :BLK_N], Asmem[stage, :, :], Bsmem[stage, :, :],
+ accum=accum, dispatch="tcgen05", cta_group=1)
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ tid = T.meta_var(warp_id * 32 + lane_id)
+
+ # === Prefetch: load first PIPE_DEPTH stages ===
+ if tid == 0:
+ for s in range(min(PIPE_DEPTH, K_TILES)):
+ tma_load(s, s * BLK_K)
+
+ # === Main loop ===
+ for k in range(K_TILES):
+ stage = k % PIPE_DEPTH
+
+ # Wait for TMA to finish loading this stage
+ T.ptx.mbarrier.try_wait(tma_bar.ptr_to([stage]), phase_tma)
+
+ # MMA on this stage's data
+ if tid == 0:
+ mma(stage, accum=(k != 0))
+
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+ phase_mma ^= 1
+
+ # Issue next prefetch load (k + PIPE_DEPTH)
+ next_k = k + PIPE_DEPTH
+ if next_k < K_TILES:
+ if tid == 0:
+ tma_load(stage, next_k * BLK_K)
+
+ # TMA phase flips when stage wraps around
+ if stage == PIPE_DEPTH - 1:
+ phase_tma ^= 1
+
+ # === TMA Store Writeback: TMEM -> RF -> Dsmem -> TMA -> GMEM ===
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+ T.cuda.cta_sync()
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ Tx.copy(Dsmem[warp_id * 32 + lane_id, 0:BLK_N], Dreg_f16[:])
+ T.ptx.fence.proxy_async("shared::cta")
+ T.cuda.warpgroup_sync(10)
+ if tid == 0:
+ Tx.copy_async(D[m_st : m_st + BLK_M, n_st : n_st + BLK_N],
+ Dsmem[:, :], dispatch="tma")
+ T.ptx.cp_async.bulk.commit_group()
+ T.ptx.cp_async.bulk.wait_group(0)
+ T.cuda.warpgroup_sync(10)
+
+ # Deallocate TMEM
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
+
(chap_persistent_kernel)=
-# 用 TMA 为 GEMM 建立 Pipeline
+## Step 6:Persistent Kernel + Tile Scheduler
+
+到目前为止,我们优化的都是单个 tile 内部的工作。Step 6 改变问题的尺度,开始跨 tile 优化。
+
+Step 5 为每个 128 x 128 output tile launch 一个 CTA。对于 4096 x 4096 输出,这意味着 1024 个独立 CTA,每个都支付自己的 setup cost,并在完成 tile 后立刻消失。
+
+Step 6 改为 launch 固定数量的 CTA pool,然后让每个 CTA 依次处理多个 tile。这带来两个收益:setup work 被摊销到多个 tile 上;tile assignment 进入 kernel 内部,scheduler 可以选择复用 operand 的顺序。我们仍然保持完整 M=N=K=4096 规模。
+
+> **本 step 改变的内容:Scope**
+> - Scope:固定数量的 persistent CTA,每个 CTA 通过 scheduler loop 处理多个 output tile。
+> - Layout:不变,同样的 per-tile SMEM/TMEM/register 路径。
+> - Dispatch:不变。
+
+### Persistent Scheduling
+
+Persistent kernel 的定义性思想是按硬件规模而不是问题规模设置 grid。它 launch `SM_COUNT` 个 CTA,大致每个 SM 一个,不论 output tile 实际有多少,目标是让每个 SM 持续有工作。这里故意说“大致”:精确 1:1 residency 并不保证,因为它取决于 occupancy,也取决于硬件如何调度 CTA。
+
+在这里目标的 B200 上,`SM_COUNT=148`。这 148 个 CTA 中的每一个,都会循环处理 `ClusterPersistentScheduler2D` 分配给它的 tile。
+
+第一个收益是摊销。TMEM allocation、barrier initialization 和 scheduler state 现在每个 CTA 只发生一次,并在这个 CTA 处理的大约 7 个 tile 之间复用,而不是在 1024 个一次性 CTA 上反复执行。
+
+第二个收益来自 scheduler 选择的顺序。设置 `l2_group_size=8` 会把邻近 tile 分组在一起,因此共享同一 row band 的 tile 会复用同样的 A row-tile,共享同一 column band 的 tile 会复用同样的 B tile。连续运行这些 tile 可以让 operand 保持在 L2 中,而不是从 HBM 重新 fetch。这正是 Step 3 没有利用的复用。
+
+```python
+bx = T.cta_id([SM_COUNT]) # 1D grid, one CTA per SM
+
+tile_scheduler = ClusterPersistentScheduler2D(
+ "ts",
+ num_m_tiles=M // BLK_M,
+ num_n_tiles=N // BLK_N,
+ l2_group_size=8, # Group 8 nearby tiles together
+ num_clusters=SM_COUNT
+)
+tile_scheduler.init(bx)
+```
+
+循环处理多个 tile 带来一个容易忽略的 correctness consequence。每个 tile 都运行自己全新的 K-loop,这意味着它的 barrier phase 必须从已知状态开始。Step 5 中一个 CTA 只处理一个 tile,因此只初始化一次 `phase_tma` 和 `phase_mma` 完全没问题。Step 6 中,这些 initializer 必须移动到 `while tile_scheduler.valid()` loop *内部*,让每个 tile 都用与自己 TMA 和 MMA work 匹配的 phase state 开始,而不是继承上一个 tile 恰好留下的状态:
+
+```python
+while tile_scheduler.valid():
+ phase_tma: T.int32 = 0
+ phase_mma: T.int32 = 0
+ ...
+```
+
+### 完整 Kernel
+
+结构上,这个 kernel 只是把 Step 5 的 pipeline 包在 tile-level outer loop 中。唯一新的依赖是 scheduler 本身,我们和其他内容一起 import:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+from tvm.tirx.lang.tile_scheduler import ClusterPersistentScheduler2D
+```
+
+Grid dimension 现在只是 `SM_COUNT`,而不再是 `(M//BLK_M, N//BLK_N)`;`ClusterPersistentScheduler2D` 接管了给每个 CTA 分配 tile 的工作:
+
+```python
+SM_COUNT = 148 # Number of SMs on NVIDIA B200 GPU
+PIPE_DEPTH = 2
+
+def hgemm_v6(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+ F16_SIZE = 2
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ K_TILES = K // BLK_K
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (PIPE_DEPTH, BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (PIPE_DEPTH, BLK_N, BLK_K))
+ D_layout = tma_shared_layout(d_type, SwizzleMode.SWIZZLE_128B_ATOM,
+ (BLK_M, BLK_N))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ # 1D grid: one CTA per SM (not a 2D grid anymore!)
+ bx = T.cta_id([SM_COUNT])
+ wg_id = T.warpgroup_id([1])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- SMEM allocation (same as Step 5) ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ tma_bar = pool.alloc((PIPE_DEPTH,), "uint64", align=8)
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((PIPE_DEPTH, BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((PIPE_DEPTH, BLK_N, BLK_K), b_type, layout=B_layout)
+ Dsmem = pool.alloc((BLK_M, BLK_N), d_type, layout=D_layout)
+ pool.commit()
+
+ # --- Barrier + TMEM init (same as Step 5) ---
+ if warp_id == 0 and lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ for s in range(PIPE_DEPTH):
+ T.ptx.mbarrier.init(tma_bar.ptr_to([s]), 1)
+ if warp_id == 0:
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), acc_type, scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)])
+ )
+
+ # Tile scheduler: assigns tiles to CTAs in L2-friendly order
+ tile_scheduler = ClusterPersistentScheduler2D(
+ "ts",
+ num_m_tiles=M // BLK_M,
+ num_n_tiles=N // BLK_N,
+ l2_group_size=8,
+ num_clusters=SM_COUNT
+ )
+ tile_scheduler.init(bx)
+
+ tid = T.meta_var(warp_id * 32 + lane_id)
+
+ @T.inline
+ def tma_load(stage, k_offset, m_st, n_st):
+ tma_config = T.meta_var({
+ "dispatch": "tma", "cta_group": 1,
+ "mbar": tma_bar.ptr_to([stage])
+ })
+ Tx.copy_async(Asmem[stage, :, :],
+ A[m_st:m_st+BLK_M, k_offset:k_offset+BLK_K],
+ **tma_config)
+ Tx.copy_async(Bsmem[stage, :, :],
+ B[n_st:n_st+BLK_N, k_offset:k_offset+BLK_K],
+ **tma_config)
+ T.ptx.mbarrier.arrive.expect_tx(
+ tma_bar.ptr_to([stage]),
+ (BLK_M * BLK_K + BLK_N * BLK_K) * F16_SIZE)
+
+ @T.inline
+ def mma(stage, accum):
+ Tx.gemm_async(tmem[:, :BLK_N], Asmem[stage, :, :], Bsmem[stage, :, :],
+ accum=accum, dispatch="tcgen05", cta_group=1)
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ # === Outer loop: iterate over tiles ===
+ while tile_scheduler.valid():
+ # Get current tile position from scheduler
+ m_st = T.meta_var(tile_scheduler.m_idx * BLK_M)
+ n_st = T.meta_var(tile_scheduler.n_idx * BLK_N)
+
+ # === Inner loop: same pipeline as Step 5 ===
+ phase_tma: T.int32 = 0
+ phase_mma: T.int32 = 0
+
+ # Prefetch first PIPE_DEPTH stages
+ if tid == 0:
+ for s in range(min(PIPE_DEPTH, K_TILES)):
+ tma_load(s, s * BLK_K, m_st, n_st)
+
+ # Main K-loop
+ for k in range(K_TILES):
+ stage = k % PIPE_DEPTH
+ T.ptx.mbarrier.try_wait(tma_bar.ptr_to([stage]), phase_tma)
+ if tid == 0:
+ mma(stage, accum=(k != 0))
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+ phase_mma ^= 1
+ next_k = k + PIPE_DEPTH
+ if next_k < K_TILES:
+ if tid == 0:
+ tma_load(stage, next_k * BLK_K, m_st, n_st)
+ if stage == PIPE_DEPTH - 1:
+ phase_tma ^= 1
+
+ # === TMA Store Writeback: TMEM -> RF -> Dsmem -> TMA -> GMEM ===
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+ T.cuda.cta_sync()
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ Tx.copy(Dsmem[warp_id * 32 + lane_id, 0:BLK_N], Dreg_f16[:])
+ T.ptx.fence.proxy_async("shared::cta")
+ T.cuda.warpgroup_sync(10)
+ if tid == 0:
+ Tx.copy_async(D[m_st : m_st + BLK_M, n_st : n_st + BLK_N],
+ Dsmem[:, :], dispatch="tma")
+ T.ptx.cp_async.bulk.commit_group()
+ T.ptx.cp_async.bulk.wait_group(0)
+ T.cuda.warpgroup_sync(10)
+
+ T.cuda.cta_sync()
+ tile_scheduler.next_tile() # Move to next tile
+
+ # Deallocate TMEM
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
-> 翻译状态:待翻译。对应英文章节:`chapter_gemm_async/index.md`。
+## 练习
-本页用于放置 GEMM TMA pipeline 相关内容的中文翻译。
+1. 在 Step 4 中,`arrive.expect_tx` 使用 `(BLK_M * BLK_K + BLK_N * BLK_K) * 2` byte。如果这个 byte count 太小或太大,mbarrier 会等待什么?
+2. 在 Step 5 中,为什么每个 SMEM stage 都需要自己的 TMA barrier,而不是两个 stage 共用同一个 `tma_bar`?
+3. 在 Step 6 中,一个 4096 x 4096 output 且 `BLK_M=BLK_N=128` 时有多少 output tile?如果 `SM_COUNT=148`,每个 persistent CTA 平均处理多少 tile?
diff --git a/zh/chapter_gemm_basics/index.md b/zh/chapter_gemm_basics/index.md
index 0fa614b3..5892b5d8 100644
--- a/zh/chapter_gemm_basics/index.md
+++ b/zh/chapter_gemm_basics/index.md
@@ -1,13 +1,602 @@
----
-orphan: true
+(chap_gemm_basics)=
+# 构建 Tiled GEMM
+
+:::{admonition} 概览
+:class: overview
+
+- 从 TIRx tile primitive 出发,构建一个正确的 tiled GEMM,起点是单个 output tile。
+- Step 1 是 single-tile GEMM,Step 2 加入 K-loop accumulation,Step 3 在 CTA 之间做 spatial tiling 以覆盖完整矩阵。
+- 先保证正确性;性能优化交给接下来的两章。
+:::
+
+GEMM 是本书围绕构建的核心 workload。它位于 linear layer、attention projection 和 convolution 的底层,而这些操作占据了 GPU 的大部分时间。因此,一个只“正确”的 GEMM 和一个“快速”的 GEMM 之间的差别,往往就是芯片大部分算力空转和接近饱和之间的差别。
+
+这个差距太大,无法一步跨过去。一个接近饱和的 kernel 会让你同时调试内存搬运、accumulation、tiling 和 Tensor Core scheduling,而且一开始还没有可信的 baseline 可以对照。更稳妥的路径是从能产生正确答案的最小 kernel 开始,然后一次只增加一个设计决策。
+
+本章会写出第一个正确的 tiled GEMM。前面章节以抽象形式介绍了 TIRx 的 scope / layout / dispatch 模型;这里我们把它应用到真实 kernel 上。我们从一个 128 x 128 output tile 开始,然后把它扩展成可以处理完整矩阵的 kernel:先加入 K 维度 accumulation,再加入跨多个 CTA 的 spatial tiling。
+
+这是三章 GEMM 优化路径中的第一章。这条路径会从头到尾走完一个 GEMM kernel 的演化。本章只构建正确的 tiled kernel 并到此为止。下一章({ref}`chap_gemm_async`)会把 thread copy 换成 TMA,并通过 pipelining 让数据搬运和计算重叠;{ref}`chap_gemm_advanced` 会进一步加入 warp specialization 和 CTA cluster。每章都建立在前一章之上,因此 kernel 会逐步积累功能,而不是每章重新开始。
+
+阅读每个 step 时,可以把它看作对同一份三项 contract 的一次编辑:哪个 **scope** 执行操作,operand tile 使用哪个 **layout**,以及通过哪条 **dispatch** 路径执行。多数 step 都只有一个主要变化,因此我们会先用一个小卡片指出变化是什么,并标出让复用安全所需的同步细节。Step 1 会建立后续所有修改的 baseline。
+
+## GEMM
+
+GEMM 是 dense matrix multiply,位于 linear layer、attention projection 和许多 convolution 实现的底层,所以快速 GEMM kernel 几乎在任何地方都有收益。本教程中的例子使用 $D = A B^{\top}$:
+
+- $A$ 的 shape 是 $M \times K$。
+- $B$ 的 shape 是 $N \times K$。
+- $D$ 的 shape 是 $M \times N$。
+- $D[m,n] = \sum_k A[m,k] \cdot B[n,k]$。
+
+这里的 transpose 不是我们额外选择执行的操作;它来自数据的存储方式。示例保持 $B$ 为 $N$ 行、每行长度 $K$,这也是 linear-layer weight 通常使用的布局。因此沿 $K$ contraction 时,自然就是在读取 $B^{\top}$,不需要实际重排。
+
+整个教程中,我们用 TFLOPS 衡量 kernel 的吞吐,把每次 multiply-add 计为两个 floating-point operation,并除以 wall-clock time:
+
+$$\text{TFLOPS} = \frac{2 \times M \times N \times K}{t_{\text{seconds}} \times 10^{12}}$$
+
+### GEMM 数据路径
+
+本教程中的每个优化最终都归结为数据位于哪里、如何移动。因此在写代码前,先把这条路径画出来是值得的。一个 Blackwell GEMM kernel 的核心只有两类活动:在不同 memory 之间搬运 tile,以及在 tile 上计算。下图追踪一个 tile 从输入到输出会触碰的每一种 memory:
+
+
+
+上图展示了 baseline 路径。之后每个优化都会编辑这条路径,但不会替换它。从左到右读:operand tile 先从 GMEM 移到 SMEM;随后 `tcgen05.mma` 消费 SMEM operand,并把 accumulator 写入 TMEM;最后 epilogue 把 TMEM 读回寄存器,再把结果 store 到 GMEM。请记住这条链路,因为下面每一步都会改变其中某一跳*如何*发生,但不会改变这些跳本身。
+
+## 优化路径
+
+上面的朴素数据路径已经足以得到正确答案,但会让大部分硬件空闲。教程剩余部分会一次加入一个 Blackwell 特性来缩小这个差距,每个特性都通过 TIRx tile primitive 表达。我们将依次经过这些特性:
+
+- **TMA async movement** 通过 Blackwell 的硬件 copy 路径移动 GMEM <-> SMEM tile,并用 barrier 跟踪完成。
+- **Software pipelining** 使用多个 SMEM stage,让下一块 K tile 的数据搬运可以与当前 tile 上的 Tensor Core compute 重叠。
+- **Persistent scheduling** 保持一组固定 CTA,让每个 CTA 通过 tile scheduler 处理多个 output tile,而不是每个 tile launch 一个 CTA。
+- **Warp specialization** 把 producer、MMA consumer 和 writeback 角色拆分到不同 warpgroup 上。
+- **CTA clusters** 让两个 CTA 协作处理一个更大的 Blackwell MMA tile。
+- **Multi-consumer execution** 使用多个 consumer warpgroup 同时计算 tile 的不同部分,提高 compute density。
+
---
-(chap_gemm_basics)=
(chap_single_tile)=
+## Step 1:顺序 Single-Tile GEMM
+
+仍然能覆盖完整硬件路径的最简单 GEMM,是计算单个 output tile 的 GEMM。因此我们从这里开始。Step 1 计算一个 128 x 128 output tile,K = 64;这个规模小到不需要 loop,并且数据路径中的每个部分都只出现一次。没有重复结构时,我们可以先单独看清每一跳,然后再开始推理循环。
+
+> **本 step 建立的内容:baseline**
+> - Scope:一个 128 线程的 single warpgroup 按顺序走完整条路径,一阶段接一阶段。
+> - Layout:A 和 B tile 位于 SMEM,accumulator 位于 TMEM,结果通过寄存器 staged out。
+> - Dispatch:同步 `Tx.copy` 执行 load,`tcgen05` 执行 MMA。
+
+### Single-Tile Dataflow
+
+Baseline contract 固定后,下一件事是确定一个 tile 按什么顺序穿过它。第一个 kernel 会完整走一次核心 GEMM 数据路径,也就是 data-flow 图里的同一条 GMEM -> SMEM -> TMEM -> registers -> GMEM 链路,外面没有包任何 loop。它分配工作内存、加载 operand、计算乘积、写回结果,并清理自己使用的资源:
+
+1. **Allocate**:SMEM(pool allocator)、TMEM(`tcgen05.alloc`)、mbarrier
+2. **Load**:全部 128 个线程协作把 A 和 B tile 从 GMEM copy 到 SMEM(sync `Tx.copy`)
+3. **Compute**:一个 elected thread 发起 `Tx.gemm_async` + `tcgen05.commit`;所有线程在 mbarrier 上等待
+4. **Writeback**:Warpgroup 读取 TMEM -> registers;每个线程把 fp32 cast 到 fp16 并写入 GMEM
+5. **Deallocate**:释放 TMEM
+
+### 第一个 Kernel 的四个部分
+
+完整 kernel 只有几十行,但分段读更容易消化。我们会按四部分阅读它:memory allocation、同步 load、MMA dispatch 和 writeback;之后再把它们拼成一个 kernel。沿途出现的 API 名称,是第二部分介绍过的 TIRx tile-primitive 词汇({ref}`chap_tirx_primer`、{ref}`chap_tirx_layout_api`)。
+
+**Memory allocation。** Kernel 首先从 shared memory 中切出 operand 所需空间,以及 TMEM address 和 mbarrier 的位置:
+
+```python
+pool = T.SMEMPool()
+tmem_addr = pool.alloc((1,), "uint32") # TMEM address (4 bytes)
+mma_bar = pool.alloc((1,), "uint64", align=8) # mbarrier (8 bytes)
+pool.move_base_to(1024) # Skip to offset 1024
+Asmem = pool.alloc((BLK_M, BLK_K), a_type, layout=A_layout) # 128×64 fp16
+Bsmem = pool.alloc((BLK_N, BLK_K), b_type, layout=B_layout) # 128×64 fp16
+pool.commit()
+```
+
+这里有两个细节值得停一下。`pool.move_base_to(1024)` 会把 Asmem 和 Bsmem 推到 offset 1024,给前面较小的 metadata 预留低地址区域,使 bulky operand tile 落在干净边界上。`layout=A_layout` 会让 `tma_shared_layout` 提供一个 swizzled SMEM placement,这个 placement 能被 TMA 和 `tcgen05.mma` 直接读取,正是第二部分所说的 layout-as-contract。
+
+**Synchronous load。** Buffer 到位后,operand 还需要到达 SMEM。在第一个版本中,我们让 CTA 自己的线程执行 copy:
+
+```python
+Tx.cta.copy(Asmem[:, :], A[:, :])
+Tx.cta.copy(Bsmem[:, :], B[:, :])
+T.cuda.cta_sync()
+```
+
+因为这里总共只有一个 tile(M=N=128, K=64),copy 完整 A 和 B 就是整个 load。`Tx.cta.copy(...)` 让 CTA 在这次 copy 上协作,每个线程负责自己那一片数据。后面的 `T.cuda.cta_sync()` 有双重作用:它等待每个线程完成,并发布这些线程对 shared memory 的写入,因此后续 MMA 读取 `Asmem` 和 `Bsmem` 时看到的是完整 tile,而不是半填充 buffer。这个 thread-driven copy 也是我们首先会替换的东西;下一章({ref}`chap_gemm_async`)会把它换成 TMA。
+
+**MMA dispatch。** Operand 已经位于 SMEM 中,现在可以发起 MMA,并且由一个 elected thread 来做:
+
+```python
+if warp_id == 0:
+ if T.ptx.elect_sync():
+ Tx.gemm_async(tmem[:, :BLK_N], Asmem[:, :], Bsmem[:, :],
+ accum=False, dispatch="tcgen05", cta_group=1)
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+```
+
+两层 guard 会分两步把 issuer 缩小到一个线程。外层 `if warp_id == 0` 只保留 warpgroup 中的 warp 0,内层 `if T.ptx.elect_sync():` 再从这个 warp 的 active lane 中选出一个。合起来,只剩一个线程执行 `Tx.gemm_async` 和 `tcgen05.commit`。
+
+这里需要明确说明单个线程意味着什么、不意味着什么,因为直觉读法很容易误导。单个 issuing thread 并不意味着单线程矩阵乘法。计算仍然是完整的 tile-level MMA:硬件会根据 SMEM operand layout 和 TMEM accumulator layout 描述的 tile 执行协作矩阵乘法。关键在于 `Tx.gemm_async` 是一个 *tile operation*,不是一条硬件指令。K = 64 tile 比硬件 MMA K-atom(`MMA_K = 16`)更宽,所以这个 tile op 会 lower 成沿 K 前进的一小段 raw `tcgen05.mma` 指令序列,而 warpgroup 会协作驱动每一条。之所以只有一个线程发起 tile op,是因为底层每条 `tcgen05.mma` 本身就是一条 cooperative op:一次 launch 驱动这个 K-atom 的 tile MMA。如果 128 个线程都发起同一段序列,同样的工作就会被 launch 128 次。最后,`accum=False` 告诉 MMA 覆盖 TMEM destination,而不是加到已有值上;这里没有先前 partial sum,所以这正是我们想要的。
+
+**Writeback。** 乘积现在位于 TMEM 中,但调用方希望在 GMEM 中得到 fp16 结果。因此 epilogue 必须先把结果通过寄存器带下来,并在途中 cast:
+
+```python
+Dreg = T.alloc_local((BLK_N,), acc_type) # per-thread fp32 register row
+Dreg_f16 = T.alloc_local((BLK_N,), d_type) # same row, cast to fp16
+Dreg_wg = Dreg.view(128, BLK_N, layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+T.ptx.tcgen05.wait.ld()
+Tx.cast(Dreg_f16[:], Dreg[:])
+m_thr = T.meta_var(m_st + warp_id * 32 + lane_id)
+Tx.copy(D[m_thr, n_st : n_st + BLK_N], Dreg_f16[:])
+```
+
+MMA 会在 TMEM 中留下一个 128 x 128 fp32 accumulator tile。使用 fp32 是有意的:GEMM 会沿 K 累加很多乘积,用更高精度保存 running sum 可以降低累积的 rounding error。但 `D` 是 fp16,所以这些值不能直接写出。它们先进入寄存器,在那里 narrow 到 fp16,然后才到达 GMEM。
+
+两个 register buffer 作用不同。`Dreg` 是每个线程自己的 `BLK_N` 元素 buffer,而 `Dreg_wg` 是同一组寄存器在所选 layout 下的 warpgroup-wide *view*:
+
+```python
+TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)])
+```
+
+这个 layout 把 tile 的第一维映射到 warpgroup 的线程上:thread 0 拥有 row 0,thread 1 拥有 row 1,一直到 row 127。第二维留在每个线程自己的 register buffer 中,因此单个线程持有自己那一行的所有列。Warpgroup 有 128 个线程,tile 有 128 行,所以 128 x 128 输出刚好分成每个线程一行。
+
+在这个 view 下读取 accumulator,正是 `Tx.wg.copy_async(Dreg_wg, tmem)` 所做的事,它会 lower 到 Blackwell TMEM load 路径 `tcgen05.ld`。由于这个 load 是异步的,任何线程触碰 `Dreg` 之前都必须先完成 `T.ptx.tcgen05.wait.ld()`;否则线程可能读取尚未被 load 填好的寄存器。
+
+Wait 返回后,每个线程私有的 `Dreg[:]` 保存自己那一条逻辑输出行的 fp32 值。线程把它们 narrow 到 `Dreg_f16` 中,计算自己负责的 global row:
+
+```python
+m_thr = T.meta_var(m_st + warp_id * 32 + lane_id)
+```
+
+然后写入 `D[m_thr, n_st:n_st + BLK_N]`。这些 row 在四个 warp 之间整齐切分:warp 0 写 rows 0-31,warp 1 写 rows 32-63,warp 2 写 rows 64-95,warp 3 写 rows 96-127。
+
+### 完整 Kernel
+
+现在把四个部分拼回一个可运行 kernel(M=N=128, K=64)。Import 先出现:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+```
+
+Kernel 包在后续 step 都会使用的 `hgemm_vX(M, N, K)` 风格中。Step 1 使用 `M=N=128, K=64`,因此 launch 中刚好有一个 output tile:
+
+```python
+def hgemm_v1(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ # MMA_M/MMA_N/MMA_K document the underlying hardware MMA tile; they are not
+ # passed to gemm_async (which derives the MMA shape from the operand and
+ # accumulator tiles), so the later steps omit them.
+ MMA_M, MMA_N, MMA_K = 128, 128, 16
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_N, BLK_K))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ # Step 1 is a single-tile kernel: M = BLK_M and N = BLK_N, so the grid
+ # is 1x1. Starting with a 1x1 grid keeps the per-CTA tile offsets
+ # (m_st, n_st) trivially zero; Steps 3+ generalise this to larger M / N.
+ bx, by = T.cta_id([M // BLK_M, N // BLK_N])
+ wg_id = T.warpgroup_id([1]) # single warpgroup, so wg_id is always 0 (unused below)
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- SMEM allocation ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((BLK_N, BLK_K), b_type, layout=B_layout)
+ pool.commit()
+
+ # --- Barrier + TMEM init (warp 0 only) ---
+ if warp_id == 0:
+ if lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), "float32", scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)])
+ )
+
+ m_st = T.meta_var(bx * BLK_M)
+ n_st = T.meta_var(by * BLK_N)
+ phase_mma: T.int32 = 0
+
+ # --- Load: all threads copy global -> shared (synchronous).
+ # With M=BLK_M and N=BLK_N the slices below cover the full matrices;
+ # the slice form is kept so the diff to Step 3 (multi-tile) is minimal.
+ Tx.cta.copy(Asmem[:, :], A[m_st:m_st + BLK_M, :])
+ Tx.cta.copy(Bsmem[:, :], B[n_st:n_st + BLK_N, :])
+ T.cuda.cta_sync()
+
+ # --- Compute: single elected thread issues MMA ---
+ if warp_id == 0:
+ if T.ptx.elect_sync():
+ Tx.gemm_async(
+ tmem[:, :BLK_N], Asmem[:, :], Bsmem[:, :],
+ accum=False, dispatch="tcgen05", cta_group=1
+ )
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+
+ # --- Writeback: TMEM -> RF -> GMEM ---
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ m_thr = T.meta_var(m_st + warp_id * 32 + lane_id)
+ Tx.copy(D[m_thr, n_st : n_st + BLK_N], Dreg_f16[:])
+
+ # --- Deallocate TMEM ---
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
+
+后面的每个 GEMM step 都会用同样方式编译、运行并检查自己,因此这个 scaffolding 只完整写一次。从此之后,我们只展示 kernel。要运行后续 step,把下面的 `hgemm_vX` 和匹配的问题规模换成对应版本即可。有一个注意事项:每次新的 Python session 只编译一个 step,尝试另一个 step 前先重启,因为这些示例会复用内部名字,而编译器持有 per-session state。
+
+```python
+import torch
+
+target = tvm.target.Target("cuda")
+device = torch.device('cuda') # gpu(0)
+
+M, N, K = 128, 128, 64
+kernel = hgemm_v1(M, N, K)
+with target:
+ ex = tvm.compile(tvm.IRModule({"main": kernel}), target=target, tir_pipeline="tirx")
+
+torch.cuda.empty_cache()
+torch.cuda.synchronize()
+A_tensor = torch.randn(M, K, dtype=torch.float16, device=device)
+B_tensor = torch.randn(N, K, dtype=torch.float16, device=device)
+D_tensor = torch.zeros(M, N, dtype=torch.float16, device=device)
+
+# ex.mod(...) takes torch tensors directly, the same call form used in every chapter.
+ex.mod(A_tensor, B_tensor, D_tensor)
+
+D_ref = (A_tensor.float() @ B_tensor.float().T).half()
+max_err = float((D_tensor - D_ref).abs().max())
+print(f"Max error vs torch reference: {max_err:.6f}")
+# Relative tolerance, like the warp-specialization and Flash Attention cells:
+# output magnitude grows with K, so a fixed absolute bound would fail at larger K.
+torch.testing.assert_close(D_tensor, D_ref, rtol=2e-2, atol=1e-2)
+print("PASS")
+
+# Optional timing for larger kernels.
+ITERS = 10
+for _ in range(3):
+ ex.mod(A_tensor, B_tensor, D_tensor)
+torch.cuda.synchronize()
+start = torch.cuda.Event(enable_timing=True)
+end = torch.cuda.Event(enable_timing=True)
+start.record()
+for _ in range(ITERS):
+ ex.mod(A_tensor, B_tensor, D_tensor)
+end.record()
+torch.cuda.synchronize()
+ms = start.elapsed_time(end) / ITERS
+tflops = 2 * M * N * K / ms / 1e9
+print(f"Performance: {ms:.3f} ms, {tflops:.1f} TFLOPS")
+```
+
+Steps 1 到 3 会刻意使用较小规模(这里是 128×128,Step 3 是 256³),使最初几个 walkthrough 容易跟上。{ref}`chap_gemm_advanced` 末尾的跨 step *End-to-End Result* 表则采用相反策略:它把每个 step,包括这个 Step 1 算法,都放到统一的 M=N=K=4096 规模下测量,因此 speedup ratio 可以直接比较。
+
+### Single-Tile Kernel 的限制
+
+这个 kernel 是正确的,这正是 Step 1 的目标,但它只在非常窄的设定下正确。这里有四个限制是有意留下的,后续优化路径会逐个解除它们:
+
+- 它只处理单个 K tile,因此不能对很大的 K 做 contraction。
+- 它只处理单个 output tile,因此 M 和 N 被固定在 128。
+- 它使用同步 GMEM -> SMEM copy,而不是 TMA。
+- 它没有重叠数据搬运和计算,所以两者不会同时运行。
+
+---
+
(chap_k_loop)=
+## Step 2:K-Loop Accumulation
+
+第一个要移除的是最小的限制。Step 1 只处理单个宽度为 64 的 K tile,但真实矩阵会沿远大于 64 的 K 做 contraction。在 Step 2 中,我们仍然只计算单个 output tile,但允许 K 跨越多个 64-wide chunk。
+
+想法很直接:对每个 chunk 重复 load -> MMA -> wait 序列,并让每次 MMA 累加到同一个 TMEM slot 中。真正需要小心的地方是同步。跨 iteration 复用同一个 mbarrier,会引入本章第一个真实 correctness hazard。如果代码跟踪了错误 phase,某次 wait 可能在对应 MMA 真正完成*之前*返回,静默破坏结果。下面的机制会精确说明这个错误如何发生,以及如何避免。
+
+> **本 step 改变的内容:Layout reuse**
+> - Scope:不变,仍然是 single warpgroup。
+> - Layout/reuse:同一对 SMEM tile 和同一个 TMEM accumulator slot 会在 K-loop 中复用。不分配新 storage;operand tile 流经固定的一对 buffer,accumulator state 保持在一个 TMEM slot 中。
+> - Synchronization:复用的 MMA barrier 必须在每个 K chunk 上推进到正确 phase,否则后续 wait 可能观察到更早的 completion。
+> - Dispatch:不变。
+
+### K-Loop 机制
+
+Step 1 只 contraction 了单个 64-wide K tile;这里我们保留它的 single output tile,但让 K 按矩阵需要的长度前进。为了覆盖大于 64 的 K,我们以 `BLK_K=64` 为 chunk 沿 K 迭代。每次 iteration 加载下一段 A 和 B 的 K-slice 到 SMEM,并发起 `Tx.gemm_async`。`accum` flag 把这些 chunk 拼成同一个 dot product:第一个 chunk 上 `accum=False` 初始化 TMEM accumulator,之后每个 chunk 上 `accum=True` 把该 chunk 的乘积加到 TMEM 中已经存在的 running sum 上。
+
+同步是需要谨慎的地方。我们为每次 MMA completion 复用同一个 mbarrier,而安全复用的关键是跟踪正在等待哪个 barrier phase。一个 mbarrier 带有 1-bit phase,可以是 0 或 1;每当期望的 arrival 到达,它就翻转到另一个值。微妙之处在于 wait 条件本身:`try_wait(bar, phase)` 会阻塞,直到 barrier 的内部 phase *不同于* `phase` 参数。因此我们传入的参数必须命名我们期望离开的 phase,而不是等待抵达的 phase:
+
+| K iteration | Wait 前本地 `phase_mma` | `try_wait` 等待什么 | Wait 后本地更新 |
+|---|---:|---|---:|
+| 0 | 0 | barrier flips to 1 | `phase_mma = 1` |
+| 1 | 1 | barrier flips to 0 | `phase_mma = 0` |
+| 2 | 0 | barrier flips to 1 | `phase_mma = 1` |
+
+`phase_mma ^= 1` 这一行正是保持这张表正确的原因。去掉它后,第二次 iteration 仍然调用 `try_wait(bar, 0)`,但 barrier 在第一次 MMA 后已经翻到了 phase 1,因此 wait 看到 mismatch 就立即返回,而此时第二次 MMA 还没完成。Kernel 随后会读取半计算的 accumulator,并在没有任何 error 的情况下给出错误答案。这个 bug 可以完美编译和运行,这就是 phase flip 值得如此强调的原因。
+
+### 完整 Kernel
+
+下面的完整 kernel 只是 Step 1 加入 K-loop 和 phase flip。Import 和之前相同:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+```
+
+它包在 `hgemm_v2(M, N, K)` 中。Grid 仍然是 `[1, 1]`,因为我们仍然只计算单个 output tile;增长的只是 K extent:
+
+```python
+def hgemm_v2(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ K_TILES = K // BLK_K
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_N, BLK_K))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ bx, by = T.cta_id([M // BLK_M, N // BLK_N]) # still one output tile (M=N=128)
+ wg_id = T.warpgroup_id([1])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((BLK_N, BLK_K), b_type, layout=B_layout)
+ pool.commit()
+
+ if warp_id == 0:
+ if lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), "float32", scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)]))
+
+ phase_mma: T.int32 = 0
+ m_st = T.meta_var(bx * BLK_M)
+ n_st = T.meta_var(by * BLK_N)
+
+ # === K-loop: iterate over K in chunks of BLK_K ===
+ for i in T.serial(K_TILES): # serial device loop (keeps the full-K A/B parameters correctly shaped)
+ # Load the i-th K chunk
+ Tx.cta.copy(Asmem[:, :], A[:, i*BLK_K:(i+1)*BLK_K])
+ Tx.cta.copy(Bsmem[:, :], B[:, i*BLK_K:(i+1)*BLK_K])
+
+ T.cuda.cta_sync()
+
+ # MMA: accum=False for first tile, True for rest
+ if warp_id == 0:
+ if T.ptx.elect_sync():
+ Tx.gemm_async(tmem[:, :BLK_N], Asmem[:, :], Bsmem[:, :],
+ accum=(i != 0), dispatch="tcgen05", cta_group=1)
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ # Wait for MMA, then flip phase
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+ phase_mma ^= 1
+
+ # === Writeback (same as Step 1) ===
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ m_thr = T.meta_var(m_st + warp_id * 32 + lane_id)
+ Tx.copy(D[m_thr, n_st : n_st + BLK_N], Dreg_f16[:])
+
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
+
+---
+
(chap_spatial_tiling)=
-# 构建 Tiled GEMM
+## Step 3:Spatial Tiling(Multi-CTA)
+
+K-loop 处理了 contraction dimension,但 M 和 N 仍然固定在单个 128 x 128 tile 上。真实输出远大于一个 tile,所以基础 kernel 的最后一块是用多个 tile 同时覆盖 M 和 N。Step 3 会 launch 一个 2D CTA grid,每个 output tile 一个 CTA,让 GPU 并行计算所有 tile。示例使用 M=N=K=256,对应 2x2 tile grid,刚好能让 indexing 不再平凡,又不至于把重点埋掉。
+
+> **本 step 改变的内容:Scope**
+> - Scope:一个 2D CTA grid,每个 CTA 拥有一个 128 x 128 output tile。
+> - Layout:不变;在每个 CTA 内部,这仍然是 Step 2 的同一条 SMEM/TMEM/register 路径。
+> - Dispatch:不变。
+
+### Grid Mapping
+
+Grid shape 直接来自 tiling:每个 128 x 128 output tile 一个 CTA,所以总共需要 `[M // BLK_M, N // BLK_N]` 个 CTA。相对 Step 2,唯一真正新增的工作,是让每个 CTA 知道矩阵中哪一片是*自己*要计算的 slice。
+
+CTA `(bx, by)` 拥有这个 output region:
+
+```text
+D[bx * BLK_M : (bx + 1) * BLK_M,
+ by * BLK_N : (by + 1) * BLK_N]
+```
+
+为了产生它,该 CTA 的 K-loop 会反复加载自己 A row band 和 B column band 对应的 K-slice:
+
+```text
+A[bx * BLK_M : (bx + 1) * BLK_M, k : k + BLK_K]
+B[by * BLK_N : (by + 1) * BLK_N, k : k + BLK_K]
+```
+
+Indexing 直接来自 `D = A @ B.T` 约定:`bx` 选择 A 和 D 的行,而 `by` 选择 B 的行;transpose 应用之后,这些 B 行会变成 D 的列。
+
+每个 CTA 一个 tile 是最简单可行的映射,但它也浪费。Row 中的每个 CTA 都会从 GMEM 重新加载同样的 A tile,column 中的每个 CTA 都会重新加载同样的 B tile,因此没有复用邻近 CTA 已经拉进来的数据。我们暂时保留这个浪费;persistent scheduling({ref}`chap_gemm_async` 中的 Step 6)会回到这个问题,并让这些共享 operand 在 L2 中保持 hot。
+
+**Try with your agent**:设 `M=N=K=256`、`BLK_M=BLK_N=128`、`BLK_K=64`,让它 trace CTA `(1, 0)` 和 CTA `(0, 1)`。对每个 CTA,列出 `m_st`、`n_st`、每次 K iteration 加载的 A/B slice,以及写入的 D region。哪些 B row 因为 kernel 计算 `D = A @ B.T` 而变成 D column?
+
+### 完整 Kernel
+
+这个 kernel 再次从 Step 2 发展而来,这次只有两个变化:grid shape 和 per-CTA offset。内部 K-loop 和 writeback 不变。Import 相同:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+```
+
+Grid 从 `[1, 1]` 变成 `[M // BLK_M, N // BLK_N]`,load 和 store 现在都会加上 CTA 自己的 `m_st` 和 `n_st` offset:
+
+```python
+def hgemm_v3(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ K_TILES = K // BLK_K
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_N, BLK_K))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ # 2D grid: one CTA per 128x128 output tile
+ bx, by = T.cta_id([M // BLK_M, N // BLK_N])
+ wg_id = T.warpgroup_id([1])
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((BLK_N, BLK_K), b_type, layout=B_layout)
+ pool.commit()
+
+ if warp_id == 0:
+ if lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), "float32", scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)]))
+
+ phase_mma: T.int32 = 0
+
+ # Per-CTA tile offsets
+ m_st = T.meta_var(bx * BLK_M)
+ n_st = T.meta_var(by * BLK_N)
+
+ # K-loop with offset A and B slices
+ for i in T.serial(K_TILES): # serial device loop (keeps the full-K A/B parameters correctly shaped)
+ Tx.cta.copy(Asmem[:, :], A[m_st:m_st+BLK_M, i*BLK_K:(i+1)*BLK_K])
+ Tx.cta.copy(Bsmem[:, :], B[n_st:n_st+BLK_N, i*BLK_K:(i+1)*BLK_K])
+
+ T.cuda.cta_sync()
+
+ if warp_id == 0:
+ if T.ptx.elect_sync():
+ Tx.gemm_async(tmem[:, :BLK_N], Asmem[:, :], Bsmem[:, :],
+ accum=(i != 0), dispatch="tcgen05", cta_group=1)
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+ phase_mma ^= 1
+
+ # Writeback to the correct output tile
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ m_thr = T.meta_var(m_st + warp_id * 32 + lane_id)
+ Tx.copy(D[m_thr, n_st:n_st+BLK_N], Dreg_f16[:])
+
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
-> 翻译状态:待翻译。对应英文章节:`chapter_gemm_basics/index.md`。
+## 练习
-本页用于放置基础 tiled GEMM 构建过程的中文翻译。
+1. 在 Steps 1-3 中,`Tx.copy` 会在 MMA 之前把 A 和 B tile 移入 SMEM。为什么 kernel 需要在 `Tx.gemm_async` 读取这些 SMEM tile 前执行 `T.cuda.cta_sync()`?
+2. 在 Step 2 中,如果从 K-loop 中移除 `phase_mma ^= 1` 会发生什么?Kernel 是否会等待每一次 MMA,还是后续 wait 可能过早通过?
+3. 对于 M=N=4096 且 BLK_M=BLK_N=128 的情况,Step 3 会 launch 多少个 CTA?哪些 operand tile 在逻辑上会被相邻 CTA 复用?Step 3 是否利用了这种复用?
diff --git a/zh/chapter_intro_tirx/index.md b/zh/chapter_intro_tirx/index.md
index a29ed160..4c6e3f96 100644
--- a/zh/chapter_intro_tirx/index.md
+++ b/zh/chapter_intro_tirx/index.md
@@ -1,10 +1,222 @@
----
-orphan: true
----
-
(chap_tirx_primer)=
# TIRx 入门
-> 翻译状态:待翻译。对应英文章节:`chapter_intro_tirx/index.md`。
+:::{admonition} 概览
+:class: overview
+
+- TIRx 是一个用于在 IR 层编写 GPU kernel 的 Python DSL:你会直接命名硬件,但通过结构化 IR 来表达。
+- 每个 tile 操作都由三个设计要素控制:*scope*(哪些线程执行)、*layout*(tile 位于哪里)和 *dispatch*(走哪条硬件路径)。
+- 一个可运行的 single-MMA GEMM 会同时展示这三者;本书后续内容就是把这些设计要素放大到真实规模。
+:::
+
+:::{admonition} 运行示例
+:class: note
+
+这些示例需要 Blackwell GPU(`sm_100a`,例如 B200)。TIRx 编译器作为 Apache TVM wheel 中的 `tvm.tirx` 模块发布;请和 CUDA 版本的 PyTorch 一起安装:
+
+```bash
+pip install apache-tvm
+```
+
+用 `python -c "import tvm, tvm.tirx; print(tvm.__version__)"` 确认它可以 import。同样的环境可以运行本书所有可运行示例。
+:::
+
+第一部分解释了硬件是什么。要让硬件真正计算,我们还需要一种编程方式。
+
+我们可以直接写 CUDA 或 PTX,很多快速 kernel 也确实是这样写的。问题在于,真正决定 kernel 行为的决策在那里很难看清:哪些线程执行某个操作、每个数据 tile 位于哪里、以及由哪条硬件路径执行。这些选择会埋在 intrinsic 参数、地址计算和约定之中。
+
+TIRx(Tensor IR neXt)是一个 Python DSL,它把这三个决策明确提升出来:**scope**(哪些线程执行操作)、**layout**(operand tile 位于哪里)和 **dispatch**(使用哪条硬件路径执行)。它仍然直接命名硬件概念,包括线程、shared memory、tensor memory、barrier 和 `tcgen05` MMA。区别是,这些选择现在变成结构化 IR,编译器可以 lower、检查和调度。
+
+我们不会先抽象地介绍这些概念,而是从一个完整 kernel 开始:最小 single-MMA GEMM。我们先让它跑起来,然后逐行读回去,看 scope、layout 和 dispatch 分别如何塑造它,以及 kernel 如何被编译。Kernel 依赖的 tensor layout 模型会在 {ref}`chap_tirx_layout_api` 中单独展开,完整语言特性集合在 {ref}`chap_language_reference` 中介绍;这里我们聚焦这一个 kernel 和三个设计要素。
+
+## 第一个 Kernel:Single-MMA GEMM
+
+我们承诺的 kernel 是一个最小 GEMM,删减到仍然能使用 Tensor Core 的最小版本。它计算 `D = A B^T` 的单个 128 x 128 output tile,K = 64。整个计算从头到尾被表达成一次 `Tx.gemm_async` tile operation。(这一个 tile operation 并不映射到单条硬件指令:因为硬件 MMA 的 K atom 是 16,K=64 的 tile 会 lower 成沿 K 前进的一小段 `tcgen05.mma` 指令序列。DSL 的重点正是我们写 tile,而不是手写这段序列。)在这个操作周围,kernel 做常规工作:分配 shared memory(SMEM)和 tensor memory(TMEM),把 A 和 B 从 global copy 到 shared memory,发起 tile MMA 并把结果写入 TMEM accumulator,再把 accumulator 通过寄存器读回并 store 结果。虽然它很小,但这个 kernel 就是 {ref}`chap_gemm_basics` 中 GEMM 阶梯的 Step 1,那里会完整讲解它。
+
+每个 TIRx kernel 都从同一组 import 开始,所以值得先看一次:
+
+```python
+
+import tvm
+from tvm.script import tirx as T
+from tvm.script.tirx import tile as Tx
+from tvm.tirx.cuda.operator.tile_primitive.tma_utils import tma_shared_layout, SwizzleMode
+from tvm.tirx.layout import TileLayout, S, TLane, TCol, tid_in_wg
+```
+
+我们把 kernel 包在一个小 builder `hgemm_v1(M, N, K)` 中,它接受问题 shape 并返回一个 `PrimFunc`。对于我们选择的 shape,`M=N=128, K=64`,launch 中刚好只有一个 output tile,这让第一个版本足够简单,可以一次读完:
+
+```python
+def hgemm_v1(M, N, K):
+ a_type = tvm.DataType("float16")
+ b_type = tvm.DataType("float16")
+ d_type = tvm.DataType("float16")
+ acc_type = tvm.DataType("float32")
+
+ BLK_M, BLK_N, BLK_K = 128, 128, 64
+ # MMA_M/MMA_N/MMA_K document the underlying hardware MMA tile; they are not
+ # passed to gemm_async (which derives the MMA shape from the operand and
+ # accumulator tiles), so the later steps omit them.
+ MMA_M, MMA_N, MMA_K = 128, 128, 16
+
+ A_layout = tma_shared_layout(a_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_M, BLK_K))
+ B_layout = tma_shared_layout(b_type, SwizzleMode.SWIZZLE_128B_ATOM, (BLK_N, BLK_K))
+
+ @T.prim_func
+ def kernel(
+ A: T.Buffer((M, K), a_type),
+ B: T.Buffer((N, K), b_type),
+ D: T.Buffer((M, N), d_type),
+ ):
+ T.device_entry()
+ # Step 1 is a single-tile kernel: M = BLK_M and N = BLK_N, so the grid
+ # is 1x1. Starting with a 1x1 grid keeps the per-CTA tile offsets
+ # (m_st, n_st) trivially zero; Steps 3+ generalise this to larger M / N.
+ bx, by = T.cta_id([M // BLK_M, N // BLK_N])
+ wg_id = T.warpgroup_id([1]) # single warpgroup, so wg_id is always 0 (unused below)
+ warp_id = T.warp_id_in_wg([4])
+ lane_id = T.lane_id([32])
+
+ # --- SMEM allocation ---
+ pool = T.SMEMPool()
+ tmem_addr = pool.alloc((1,), "uint32")
+ mma_bar = pool.alloc((1,), "uint64", align=8)
+ pool.move_base_to(1024)
+ Asmem = pool.alloc((BLK_M, BLK_K), a_type, layout=A_layout)
+ Bsmem = pool.alloc((BLK_N, BLK_K), b_type, layout=B_layout)
+ pool.commit()
+
+ # --- Barrier + TMEM init (warp 0 only) ---
+ if warp_id == 0:
+ if lane_id == 0:
+ T.ptx.mbarrier.init(mma_bar.ptr_to([0]), 1)
+ T.ptx.tcgen05.alloc(T.address_of(tmem_addr), n_cols=512, cta_group=1)
+
+ T.ptx.fence.proxy_async("shared::cta")
+ T.ptx.fence.mbarrier_init()
+ T.cuda.cta_sync()
+
+ tmem = T.decl_buffer(
+ (128, 512), "float32", scope="tmem", allocated_addr=tmem_addr[0],
+ layout=TileLayout(S[(128, 512) : (1@TLane, 1@TCol)])
+ )
+
+ m_st = T.meta_var(bx * BLK_M)
+ n_st = T.meta_var(by * BLK_N)
+ phase_mma: T.int32 = 0
+
+ # --- Load: all threads copy global -> shared (synchronous).
+ # With M=BLK_M and N=BLK_N the slices below cover the full matrices;
+ # the slice form is kept so the diff to Step 3 (multi-tile) is minimal.
+ Tx.cta.copy(Asmem[:, :], A[m_st:m_st + BLK_M, :])
+ Tx.cta.copy(Bsmem[:, :], B[n_st:n_st + BLK_N, :])
+ T.cuda.cta_sync()
+
+ # --- Compute: single elected thread issues MMA ---
+ if warp_id == 0:
+ if T.ptx.elect_sync():
+ Tx.gemm_async(
+ tmem[:, :BLK_N], Asmem[:, :], Bsmem[:, :],
+ accum=False, dispatch="tcgen05", cta_group=1
+ )
+ T.ptx.tcgen05.commit(mma_bar.ptr_to([0]), cta_group=1)
+
+ T.ptx.mbarrier.try_wait(mma_bar.ptr_to([0]), phase_mma)
+
+ # --- Writeback: TMEM -> RF -> GMEM ---
+ Dreg = T.alloc_local((BLK_N,), acc_type)
+ Dreg_f16 = T.alloc_local((BLK_N,), d_type)
+ Dreg_wg = Dreg.view(128, BLK_N,
+ layout=TileLayout(S[(128, BLK_N) : (1@tid_in_wg, 1)]))
+ Tx.wg.copy_async(Dreg_wg[:, :], tmem[:, :BLK_N])
+ T.ptx.tcgen05.wait.ld()
+ Tx.cast(Dreg_f16[:], Dreg[:])
+ m_thr = T.meta_var(m_st + warp_id * 32 + lane_id)
+ Tx.copy(D[m_thr, n_st : n_st + BLK_N], Dreg_f16[:])
+
+ # --- Deallocate TMEM ---
+ T.cuda.cta_sync()
+ if warp_id == 0:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=1)
+ T.ptx.tcgen05.dealloc(tmem_addr[0], n_cols=512, cta_group=1)
+
+ return kernel
+```
+
+在读这个 kernel 之前,我们先确认它能工作。我们编译它,并用 torch reference 检查输出。这里不需要显式写出具体架构:arch(例如 `sm_100a`)会从设备自动检测,所以 target `"cuda"` 就足够了,`tir_pipeline="tirx"` 选择 TIRx lowering pipeline。编译完成后,`ex.mod(...)` 可以直接接受 torch tensor,中间不需要手动转换。
+
+```python
+import torch
+
+target = tvm.target.Target("cuda")
+device = torch.device('cuda') # gpu(0)
+
+M, N, K = 128, 128, 64
+kernel = hgemm_v1(M, N, K)
+with target:
+ ex = tvm.compile(tvm.IRModule({"main": kernel}), target=target, tir_pipeline="tirx")
+
+torch.cuda.empty_cache()
+torch.cuda.synchronize()
+A_tensor = torch.randn(M, K, dtype=torch.float16, device=device)
+B_tensor = torch.randn(N, K, dtype=torch.float16, device=device)
+D_tensor = torch.zeros(M, N, dtype=torch.float16, device=device)
+
+# ex.mod(...) takes torch tensors directly, the same call form used in every chapter.
+ex.mod(A_tensor, B_tensor, D_tensor)
+
+D_ref = (A_tensor.float() @ B_tensor.float().T).half()
+max_err = float((D_tensor - D_ref).abs().max())
+print(f"Max error vs torch reference: {max_err:.6f}")
+torch.testing.assert_close(D_tensor, D_ref, rtol=2e-2, atol=1e-2)
+print("PASS")
+```
+
+## Scope、Layout、Dispatch
+
+现在 kernel 已经能跑,我们可以回过头来读它,问每一行到底决定了什么。从这个角度看,整个 kernel 是围绕三个设计要素的一组选择。里面的每个操作都回答同样三个问题:*谁*执行它、它的数据*在哪里*、它*如何*执行;这三个答案就是 scope、layout 和 dispatch。本节会依次讨论这些设计要素;下面的交互 demo 可以看到每个设计要素控制了 kernel 的哪些行。
+
+```{raw} html
+
+```
+*交互图:点击 Scope / Layout / Dispatch,高亮 kernel 中由每个设计要素控制的行。*
+
+使用 demo 时,请关注三个问题:
+
+- **Scope:谁执行这个操作?** `Tx.cta.copy(...)` 是 CTA-scoped,因此全部 128 个线程都会帮助完成 GMEM -> SMEM copy。`Tx.gemm_async(...)` 由一个 elected thread 发起,因为 lowering 后的每条 `tcgen05.mma` 指令本身已经是一次 cooperative MMA launch。`Tx.wg.copy_async(...)` 是 warpgroup-scoped,因此 warpgroup 的 128 个线程会按行拆分 TMEM readback。
+- **Layout:每个 tile 位于哪里?** A 和 B 使用 `tcgen05.mma` 期望的 swizzled SMEM layout。Accumulator 位于 TMEM 中,使用 `TLane`/`TCol` 布局。Register readback view 把 row 映射到 `tid_in_wg`,因此每个 warpgroup thread 拥有一个 row fragment。
+- **Dispatch:哪条硬件路径执行它?** `Tx.gemm_async(..., dispatch="tcgen05", ...)` 选择 Blackwell Tensor Core 路径。Copy 操作也有 dispatch 选择:第一个 kernel 使用普通 thread copy,后续 GEMM step 会把这些 copy 换成 TMA,而不改变周围的 scope 或 layout。
+
+**Try with your agent**:从第一个 kernel 里挑三行:一个 copy、一个 MMA、一个 TMEM readback。让它用 scope、layout 和 dispatch 标注每一行,然后检查答案是否与 guard、buffer layout 和 `dispatch=` 参数一致。
+
+## 编译如何工作
+
+我们已经在上面编译过 kernel 来测试它;现在稍微近距离看一下这个步骤做了什么。流程很短:把 `PrimFunc` 包进一个 `IRModule`,再交给 `tvm.compile(mod, target=..., tir_pipeline="tirx")`。这会运行 TIRx lowering pipeline,并返回一个可以直接调用的 `Executable`。
+
+```python
+target = tvm.target.Target("cuda")
+ex = tvm.compile(tvm.IRModule({"main": kernel}), target=target, tir_pipeline="tirx")
+```
+
+至少大致知道 `tir_pipeline="tirx"` 会触发什么,是有帮助的。Pipeline 的核心 pass 是 `LowerTIRx`,它会根据每个 tile primitive 的 scope / layout / dispatch contract 进行解析:这里正是我们刚才讨论的三个设计要素真正兑换成指令的地方。之后,常规 host/device split 和 finalize step 会产生可 launch 的 module。如果愿意,也可以在 `with target:` block 内编译,这样 kernel 可以获取外层 target context。
+
+这个流程的一个好处是没有东西对你隐藏:结果可以在两个层级上检查。你可以用 `.show()` 或 `.script()` 读取 IR 本身,也可以从编译后的 module 中直接查看编译器最终生成的 CUDA C。
+
+```python
+kernel.show() # pretty-print the TIRx (TVMScript)
+print(kernel.script()) # ... the same, as a string
+
+# the generated CUDA C source, from the compiled Executable:
+print(ex.mod.imports[0].inspect_source())
+```
+
+这里只是一个概览。完整 lowering 过程,包括所有 pass、tile-primitive dispatch 如何解析,以及 host/device split 如何完成,请见 {ref}`chap_arch`。
+
+## 下一步
+
+一个 kernel 已经足够让我们认识 scope、layout 和 dispatch,并看到它们如何被编译和运行。这三个设计要素以及 kernel 本身,分别通向后续章节:
-本页用于放置 TIRx 入门 walkthrough 的中文翻译。
+- {ref}`chap_tirx_layout_api`:tensor layout 模型(`TileLayout`、命名轴、swizzle),上面 operand 和 accumulator 的 placement 都建立在它之上。如果 layout 这个设计要素最让你困惑,可以从这里继续。
+- {ref}`chap_language_reference`:完整语言特性集合,包括 parser utility、data type、buffer 和 memory、control flow,以及 thread synchronization;当你需要完整词汇表而不是导览时可以查这里。
+- {ref}`chap_gemm_basics`:这个 kernel 作为 GEMM 优化路径的 Step 1,并进一步加入 K-loop accumulation、spatial tiling、TMA 和 warp specialization。如果你想看同样三个设计要素如何扩展到真实 kernel,这是自然的下一站。
diff --git a/zh/chapter_layout_generations/index.md b/zh/chapter_layout_generations/index.md
index 359d216e..5367939a 100644
--- a/zh/chapter_layout_generations/index.md
+++ b/zh/chapter_layout_generations/index.md
@@ -1,79 +1,288 @@
(chap_layout_generations)=
# 跨 GPU 世代的 Tensor Core 操作数布局
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- 从 Ampere 到 Hopper 再到 Blackwell,Tensor Core 执行的高层操作仍然相同:`D = A B + C`。
+- 每一代发生变化的是:操作数如何到达 Tensor Core,支持哪些 tile shape 与 dtype,以及 accumulator 放在哪里。
+- Ampere 使用 warp-level register fragment。Shared memory tile 通过 `ldmatrix` 加载到 fragment 中,accumulator 保持在寄存器里。
+- Hopper 让 `wgmma` 通过 matrix descriptor 直接从 shared memory 读取操作数。Descriptor 会命名 Tensor Core 期望的 shared-memory swizzle format。
+- Blackwell 保留 shared-memory operand path,但把 accumulator 移到 TMEM 中。Block-scaled MMA 还会通过 TMEM staging scale factor。
+- 两个内存约束跨所有世代都始终存在:global memory coalescing 和 shared memory bank conflict。
:::
-TODO:翻译导言部分。
+从远处看,Tensor Core 操作似乎一直很稳定。它把 A 和 B 的 tile 相乘,加上 accumulator C,并产生 D。这个形式从 Volta 起就是这样。
+
+但围绕这个操作的细节并没有保持不变。在某一代上很快的 kernel,到了下一代可能会变慢。使用错误布局的 kernel 也可能算出错误答案,即使逻辑数学仍然写作 `D = A B + C`。原因是 Tensor Core 消费的不是抽象矩阵,而是非常具体的硬件布局中的操作数。
+
+本章会沿着三代硬件追踪这份布局契约。Ampere 通过 warp-level register fragment 暴露 Tensor Core。Hopper 把输入操作数移到 shared memory descriptor 上。Blackwell 保留 shared memory 操作数,但把 accumulator 移到 TMEM 中。操作仍然是 matrix-multiply-accumulate,但进入 Tensor Core 和离开 Tensor Core 的路径每一代都在改变。
+
+{ref}`数据布局 ` 一章中的布局记号,是我们描述这些契约的语言。Blackwell TMEM 的细节会在 {ref}`chap_tmem` 中单独介绍。
## 两个始终存在的约束
-TODO:翻译 “Two Constraints That Never Went Away” 小节。
+在 Tensor Core 参与之前,两个普通内存约束就已经在塑造 GPU kernel 的布局。
+
+第一个是 global memory coalescing。当一个 warp 的 32 个 lane 发起 global memory load 时,内存系统希望这些地址落在少量连续且对齐的 memory segment 中。如果地址分散,warp load 就会变成多次 memory transaction。同样的逻辑数据搬运会消耗更多带宽和更多时间。
+
+第二个是 shared memory bank conflict。Shared memory 被分成 32 个 bank。如果 warp 中多个 lane 访问映射到同一个 bank 的不同地址,这些访问无法同时服务,硬件会把它们串行化。因此,一个看起来只是扁平 shared memory array 的布局,可能因为 bank pattern 而变慢。
+
+Swizzling 是修复 shared memory 侧问题的常见方法。逻辑 tile 保持不变,但物理地址映射被重排,让访问 pattern 分散到不同 bank 上,而不是堆到同一个 bank。
+
+这两个约束即使在完全不用 Tensor Core 的 kernel 中也存在。Tensor Core kernel 会再加上第三个约束:操作数必须按照 Tensor Core 指令自身期望的布局排列。本章剩下的内容,就是看这个第三个约束如何在 Ampere、Hopper 和 Blackwell 之间变化。
## Ampere:Warp Lane 上的寄存器 Fragment
-TODO:翻译 “Ampere: Register Fragments over Warp Lanes” 小节。
+在 Ampere 级别的 GPU 上,主要 Tensor Core 指令是 warp-level 的 `mma.sync.aligned.m16n8k*` 系列。最重要的事实是这条指令从哪里读写数据:寄存器。
+
+A、B,以及 C 或 D accumulator,都是分布在 warp 的 32 个 lane 上的 per-thread register fragment。Shared memory 只是 staging area。在 MMA 运行之前,operand tile 必须从 shared memory 移到这条指令期望的精确 register fragment 布局中。
+
+数据路径如下:
+
+```text
+SMEM to registers with ldmatrix
+registers to registers with mma.sync
+registers back to SMEM with ordinary stores
+```
+
+Ampere 的大部分布局故事都来自这条路径。Kernel 必须先把 tile 以一种能高效加载的形式存入 shared memory,然后用 `ldmatrix` 产生 `mma.sync` 需要的 register fragment。
## Ampere Tensor Core 期望的输入
-TODO:翻译 “What the Ampere Tensor Core Expects” 小节。
+Ampere Tensor Core 读取由 8 by 8 subtile unit 构成的 register fragment。这些 unit 正是 `ldmatrix` 加载的单位,也是 MMA 消费的单位。
+
+以带 fp16 或 bf16 输入、fp32 accumulation 的 `mma.m16n8k16` 为具体例子。Accumulator tile 的形状是 `16 by 8`,并以固定 pattern 分布到 32 个 lane 上。
+
+对于 C 或 D accumulator,lane `l` 持有的行是:
+
+```text
+l / 4
+l / 4 + 8
+```
+
+列是:
+
+```text
+2 * (l % 4)
+2 * (l % 4) + 1
+```
+
+因此每个 lane 拥有四个 fp32 accumulator 值:来自两个 8-row half 的两行,与两个相邻列交叉组合。连续四个 lane 覆盖某一行的八个列。
+
+A operand 使用同样的 M-side row carve。K 维度分散在 `l % 4` 以及 lane 持有的寄存器中。对于 fp16 或 bf16,每个 32-bit 寄存器会打包两个 K 值。
+
+B operand 使用匹配的 K placement,并把 N 侧分散到 lane group 和寄存器中。
-## `ldmatrix`:从共享内存到寄存器 Fragment
+具体细节会随 instruction shape 和 dtype 而变,但原则固定:Tensor Core 期望一个特定的 per-lane register fragment。如果值没有按这个 pattern 放到这些寄存器里,指令就会把错误元素相乘。
-TODO:翻译 “ldmatrix: Shared Memory to Register Fragment” 小节。
+在布局记号中,m8n8 fragment 就是用命名 lane 轴写出的那类 pattern,例如:
-
+```text
+S[(8, 4, 2) : (4@laneid, 1@laneid, 1@m)]
+```
+
+两个 `laneid` 迭代器共同描述 row 和 column piece 如何分散到 lane 上,而最后的 `m` component 描述 per-lane register slot。
+
+## `ldmatrix`:从 Shared Memory 到寄存器 Fragment
+
+`ldmatrix` 是 Ampere 上连接 shared memory 与 Tensor Core register fragment 的指令。它是 warp-collective load。一条指令会把一个或多个 8 by 8 的 16-bit 矩阵从 shared memory 移到 `mma.sync` 期望的分布式 register layout 中。
+
+指令形式是:
+
+```text
+ldmatrix.sync.aligned.m8n8.x1.shared.b16
+ldmatrix.sync.aligned.m8n8.x2.shared.b16
+ldmatrix.sync.aligned.m8n8.x4.shared.b16
+```
+
+并且可以带一个可选的 `.trans` qualifier。
+
+`.x1`、`.x2` 和 `.x4` 形式分别加载一个、两个或四个 8 by 8 矩阵。Row base address 由 lane 提供。对于矩阵 `m` 和行 `r`,base address 来自 lane `m * 8 + r`。这意味着 `.x1` 使用 lane 0 到 7 作为 row address,`.x2` 使用 lane 0 到 15,`.x4` 使用 lane 0 到 31。
+
+结果会直接落入 MMA fragment。对于基本的 8 by 8 情况,lane `l` 会收到 Tensor Core 期望的 row/column pair。普通 per-lane `ld.shared` 指令循环必须手动复现这种 scatter。`ldmatrix` 则把 shared-memory 到 fragment 的重排作为一条 warp-collective 指令完成。
+
+`.trans` 形式会在加载每个 8 by 8 矩阵时做 transpose。当 operand 的存储方向与 MMA 指令期望方向相反时,会使用这个形式。
+
+
## 写回 Ampere Fragment
-TODO:翻译 “Writing the Ampere Fragment Back” 小节。
+`mma.sync` 完成之后,accumulator 仍然是 register fragment。Epilogue 必须把这个 fragment 移出去。
+
+Ampere 上没有专门的 `ldmatrix` 反向指令。Kernel 使用普通 per-thread store,有时在 store 之前配合 warp shuffle 或局部重排,把 accumulator 写入 shared memory 或 global memory 中的有用布局。
+
+这让 Ampere 模型保持简单,但也把很多布局工作暴露给 kernel。输入侧用 `ldmatrix` 创建 fragment。Compute 指令读写 register fragment。输出侧由这些 fragment 上的普通 store 处理。
## Ampere 上的 Swizzle
-TODO:翻译 “Swizzle on Ampere” 小节。
+Ampere kernel 已经需要 shared memory swizzle。原因是 shared memory tile 通常以一种访问 pattern 写入,又以另一种访问 pattern 读取。
+
+假设一个 tile 沿 row 从 global memory 填充。Row-major layout 让这种写入 coalesced 且 bank friendly。但 `ldmatrix` 稍后可能用一种等价于沿 column 或跨 8 by 8 subtile 行走的 pattern 读取这个 tile。使用普通 row-major layout 时,这些读取可能堆到同一个 shared memory bank 上。
+
+对于一个简单的 `(8, 64)` float16 tile,一行是:
+
+```text
+64 * 2 bytes = 128 bytes
+```
+
+这刚好是一整条 shared memory bank line。沿固定 column 向下走时,每行前进 128 byte,因此 bank index 会重复。八行可能全部落到同一个 bank 上,产生 8-way conflict。
+
+改成普通 column-major layout 并不能完整解决问题。它通常只是把 conflict 移到另一次访问上。Row write 变差,而 column-style read 变好。
+
+XOR swizzle 通过让物理 column 依赖 row 来解决这个问题。一个简单版本是:
+
+```text
+physical_col = logical_col xor row
+```
+
+逻辑 tile 不变。Shared memory 中的物理 placement 被重排,使 row-style write 和 Tensor Core read pattern 都能避免 bank conflict。
+
+在 Ampere 上,这种 swizzle 通常通过手写 shared memory index math 表达。后续世代会把它变成硬件 engine 使用的 descriptor format 的一部分。
-
+
-## Hopper:`wgmma`、共享内存 Descriptor 和 Swizzle Format
+## Hopper:`wgmma`、Shared Memory Descriptor 和 Swizzle Format
-TODO:翻译 “Hopper: wgmma, Shared Memory Descriptors, and Swizzle Formats” 小节。
+Hopper 改变了 Tensor Core 路径的输入侧。Hopper `wgmma` 不再要求每个 operand 都通过 `ldmatrix` 加载到寄存器,而是可以直接从 shared memory 读取 operand。
+
+B operand 从 shared memory matrix descriptor 中读取。A operand 可以从 shared memory descriptor 读取,也可以从寄存器读取,对应 `.ss` 和 `.rs` 形式。
+
+这移除了 SMEM-sourced operand 上显式的 `ldmatrix` 步骤。但它没有移除布局要求。Tensor Core 仍然期望 operand 以精确的 shared memory format 存放。区别在于,现在这个 format 通过 matrix descriptor 告诉硬件。
## Hopper Tensor Core 期望的输入
-TODO:翻译 “What the Hopper Tensor Core Expects” 小节。
+Hopper shared memory matrix descriptor 是 shared memory 中矩阵 tile 的紧凑描述。它告诉 `wgmma` 如何把逻辑 operand coordinate 转换成 shared memory address。
+
+Descriptor 包含如下字段:
+
+```text
+start address
+leading dimension offset
+stride dimension offset
+swizzle mode
+base offset
+```
+
+具体解释取决于 operand major mode。对于 K-major tile,一个 stride 沿 K 前进,另一个沿 M 前进。对于 MN-major tile,这些角色会交换。
+
+Swizzle mode 是 shared memory descriptor format 之一,例如:
-
+```text
+SWIZZLE_NONE
+SWIZZLE_32B
+SWIZZLE_64B
+SWIZZLE_128B
+```
+
+Swizzle mode 决定两件事。它决定 descriptor 使用的 atom shape,也决定在 atom 内应用的 XOR permutation。例如,128-byte swizzle mode 会把 operand 看作 8-row by 128-byte atom 的网格,并在每个 atom 内应用 swizzle。
+
+Kernel 仍然必须正确放置 byte。TMA 通常负责填充 shared memory tile,而 TMA descriptor 必须使用与后续 `wgmma` descriptor 命名的 swizzle format 相同的格式。如果 TMA 写入一个 128-byte swizzled tile,`wgmma` descriptor 就必须把它作为 128-byte swizzled tile 读取。如果 descriptor 和数据不一致,Tensor Core 就会读取被打乱的 operand。
+
+这是相对 Ampere 的主要变化。Swizzle 不再只是隐藏在手写 shared memory indexing 里。Hopper 把它变成一等 descriptor format。写 tile 的 TMA load 和读 tile 的 `wgmma` 指令都可以命名同一种 format。
+
+
## Hopper 输出仍然使用寄存器
-TODO:翻译 “Hopper Output Still Uses Registers” 小节。
+Hopper 改变了输入路径,但 accumulator 仍然位于寄存器中。
+
+一条 `wgmma` 指令会把 accumulator 写成 per-thread register fragment。具体 fragment 大小和寄存器数量取决于 instruction shape,例如 `m64nNk16` 中 N 会改变 accumulator register 的数量。但基本思想和 Ampere 相同:epilogue 消费的是 register fragment。
+
+因此,Hopper 具有混合布局模型。输入 operand 可以直接来自 shared memory descriptor,并由硬件描述 swizzle。输出 accumulator 仍然是一个 register layout 问题。
+
+Blackwell 改变了输出侧。
## Blackwell:`tcgen05` 和 TMEM
-TODO:翻译 “Blackwell: tcgen05 and TMEM” 小节。
+Blackwell 保留了数据 operand 上的 shared memory descriptor 思想。A 和 B 仍然以 Tensor Core 期望的布局准备在 shared memory 中。有些 mode 也可以从 TMEM 读取 A operand。
+
+主要变化是 accumulator。`tcgen05.mma` 会把 accumulator 写入 Tensor Memory,也就是 TMEM,而不是把它作为长生命周期 register fragment 保持住。在 compute phase 中,accumulator 位于 TMEM。Epilogue 随后用 `tcgen05.ld` 把它加载回寄存器。
+
+这把输出布局问题从寄存器移动到了 TMEM。Kernel 必须分配 TMEM,选择正确的 TMEM 布局,等待 MMA 完成,然后使用匹配的 `tcgen05.ld` 路径把 accumulator fragment 恢复出来供 epilogue 使用。
+
+`cta_group::1` 和 `cta_group::2` 如何把 accumulator 切分到一个或两个 CTA 上,会在 {ref}`chap_tensor_cores` 中介绍。与早期世代差异最大的布局,是 block-scaled scale-factor layout。
## TMEM 中的 Scale Factor Layout
-TODO:翻译 “Scale Factor Layout in TMEM” 小节。
+Block-scaled MMA mode,例如 `mxfp8` 和 `nvfp4`,会添加 scale-factor operand。除了 A 和 B,MMA 还会读取:
+
+```text
+SFA(M, SFK)
+SFB(N, SFK)
+```
+
+其中 `SFK` 是 K scale block 的数量。
+
+数据 operand A 和 B 位于 shared memory。Scale factor 位于 TMEM。因此它们有不同的数据移动路径。
+
+TMA 从 global memory 加载到 shared memory。它不会直接加载到 TMEM。因此 scale factor 通常分两步移动:
+
+```text
+global memory to shared memory with TMA
+shared memory to TMEM with tcgen05.cp
+```
-
+只有完成这次 copy 后,scale factor 才会位于 `tcgen05.mma` 期望读取的内存空间中。
+
+TMEM scale-factor layout 使用 TMEM 的硬件坐标 Lane 和 Col。在 TIRx 布局记号中,这些轴写作 `TLane` 和 `TCol`。
+
+一个 128-row scale vector 会紧凑压缩到一个 32-lane group 中,然后复制到 TMEM 的四个 32-lane window 上。在布局记号中,核心 pattern 是:
+
+```text
+S[(32, sf_per_mma) : (1@TLane, 1@TCol)] + R[4 : 32@TLane]
+```
+
+Shard 放置 base 32-row group:
+
+```text
+TLane = r
+TCol = s
+```
+
+Replica term 在 lane offset 0、32、64 和 96 处添加副本:
+
+```text
+TLane = r + 32 * q, where q in {0, 1, 2, 3}
+TCol = s
+```
+
+这就是 `warpx4` broadcast pattern。同一组紧凑 scale factor 会在完整 128-lane TMEM 空间中变得可见。
+
+32-bit `TCol` cell 内部还有 byte packing。Packing 取决于 `scale_vec` mode:
+
+```text
+1X: one scale value is broadcast across the 32-bit cell
+2X: two scale values are packed, each duplicated
+4X: four K-block scale values are packed
+```
+
+
+
+这个 packing 在 Ampere 或 Hopper 中没有直接对应物,因为那些世代没有用于 `tcgen05` block-scaled MMA 的 TMEM scale-factor operand。
+
+在 `cta_group::2` 中,scale factor 会跟随它们缩放的数据。SFA 缩放 A,因此它会按 M 在两个 CTA 之间切分,匹配每个 CTA 拥有的 A 行。SFB 缩放 B,而 B 被计算的两个 CTA half 共享,因此 SFB 会 multicast 给两个 CTA({ref}`chap_tensor_cores`)。
## 一个反复出现的 Fragment
-TODO:翻译 “A Recurring Fragment” 小节。
+虽然周围的内存路径不断变化,但有一个结构反复出现:m8n8-style register fragment。
+
+在 Ampere 上,`ldmatrix` 构造这个 fragment,供 `mma.sync` 读取。
+
+在 Hopper 上,`wgmma` 把 accumulator 写成 register fragment,供 epilogue 使用。
+
+在 Blackwell 上,accumulator 在 compute 期间位于 TMEM,但 `tcgen05.ld` 会在 epilogue 处理和存储之前,把它重新加载到 register fragment 中({ref}`chap_tmem`)。
+
+所以 fragment 并没有消失,它的角色变了。早期世代会让 accumulator 在整个 compute phase 中都留在这里。Blackwell 主要在 TMEM 与 epilogue 之间的边界上使用它。
## 主线
-TODO:翻译 “The Throughline” 小节。
+在 Ampere 上,kernel 显式构建 Tensor Core register fragment。Shared memory swizzle 大多由 kernel 通过 index math 负责。
+
+在 Hopper 上,Tensor Core 可以通过 matrix descriptor 直接从 shared memory 读取 operand。Swizzle 变成 TMA 和 `wgmma` 共享的命名 descriptor format。
+
+在 Blackwell 上,输入侧仍然使用 shared memory operand,但 accumulator 移到 TMEM 中。Block-scaled MMA 还添加了必须 staged 到 TMEM 的 scale-factor operand。
+
+Descriptor 并不会消除布局工作。它们只是把契约显式化。Kernel 仍然必须确保数据搬运路径、内存布局和 Tensor Core 指令全都一致。写入 swizzled SMEM tile 的 TMA descriptor、读取这个 tile 的 MMA descriptor,以及附着在 buffer 上的布局,都必须描述同一个物理排列。
+
+如果其中任意一项不一致,硬件仍然会运行。它只会读到错误的 byte,或者以很慢的方式读取它们。这就是为什么布局不是 Tensor Core kernel 周围的装饰;它是指令接口的一部分。
diff --git a/zh/chapter_tensor_cores/index.md b/zh/chapter_tensor_cores/index.md
index fc752c80..35949513 100644
--- a/zh/chapter_tensor_cores/index.md
+++ b/zh/chapter_tensor_cores/index.md
@@ -1,84 +1,187 @@
(chap_tensor_cores)=
# Tensor Core:`tcgen05`
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- `tcgen05` 是 Blackwell 的 Tensor Core 指令族。它的 MMA 指令会协作执行 tile matrix-multiply-accumulate 工作,并由一个被选中的线程 commit 指令。
+- Accumulator 位于 TMEM,而不是寄存器。Epilogue 随后用 `tcgen05.ld` 把它带回寄存器。
+- `cta_group::1` 和 `cta_group::2` 控制一个 CTA 还是两个 CTA 协作执行 MMA。这个选择也会改变 M 维度映射到 TMEM 的方式。
+- Block-scaled MMA mode,例如 `mxfp8` 和 `nvfp4`,会添加 scale-factor operand。数据 operand 位于 SMEM,而 scale factor 会通过 TMEM staged。
:::
-TODO:翻译导言部分。
+Dense linear algebra 是现代 GPU 花费最多有效工作的地方。普通 CUDA core 矩阵乘法无法接近芯片标称峰值({ref}`chap_background`)。快速 GEMM 和 attention kernel 通过给 Tensor Core 喂入正确的 tile shape、layout 和 synchronization,才能达到这个峰值。
+
+基本操作从 Volta 以来在精神上没有改变。Tensor Core 消费矩阵 tile,相乘,并把结果累加起来。每一代变化的是操作如何发起、操作数如何布局,以及 accumulator 位于哪里。
+
+Blackwell 对最后一部分做了很大改变。`tcgen05` 的 accumulator 不再作为长生命周期 register fragment 保存。它会写入 Tensor Memory,也就是 TMEM({ref}`chap_tmem`)。这一个变化会影响整个 kernel。MMA 写入 TMEM。完成状态被异步跟踪。Epilogue 随后从 TMEM 中加载 accumulator,并把它重新变成用于转换和 store 的 register fragment。
+
+本章聚焦 compute instruction 本身。TMA({ref}`chap_tma`)负责把 operand 移入 SMEM。TMEM 负责保存 accumulator 和某些 scale-factor operand。`tcgen05.mma` 是位于这两类数据移动之间的 Tensor Core 操作。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
```
-
-*点击图中组件查看细节:TODO:翻译 tcgen05 and Tensor Memory 图注。*
+*交互图:`tcgen05` accumulator 行为。切换 A 或 B 的 transpose,选择输出宽度 `N`,并逐步执行 `K` iteration,观察 partial sum 如何在 TMEM 中累加。*
## `tcgen05` MMA
-TODO:翻译 “The tcgen05 MMA” 小节。
+`tcgen05` MMA 是 Blackwell Tensor Core 的 matrix-multiply-accumulate 指令。它是一条协作指令。工作由一个 warpgroup 执行,并且在某些 mode 中可以涉及同一 cluster 中的两个 CTA。指令不是由每个线程独立发起的。一个被选中的线程会代表参与的 group commit 这个操作。
+
+把 MMA 拆成三个问题来看会更清楚。
+
+第一个问题是谁参与协作。普通 mode 使用一个 CTA,写作 `cta_group::1`。更大的 mode 使用 cluster 中的两个 CTA,写作 `cta_group::2`。在两种情况下,这条指令都表示一个 tile 上的一次 Tensor Core 操作,而不是某个线程执行的 scalar operation。
+
+第二个问题是 operand 和 result 位于哪里。数据 operand 通常位于 SMEM。某些变体也可以从 TMEM 读取 A operand。Accumulator 写入 TMEM。Operand layout 必须匹配 Tensor Core 的期望,包括数据 operand 使用的 swizzled shared-memory layout({ref}`chap_data_layout`)。
+
+第三个问题是如何观察完成。`tcgen05.mma` 是异步的。发起 MMA 并不表示 multiply-accumulate 已完成。指令在操作 commit 后返回,而 Tensor Core 会继续运行。Kernel 使用 commit group 和 `mbarrier` 来知道结果何时就绪({ref}`chap_async_barriers`)。
+
+这种异步行为让 overlap 成为可能。快速 kernel 不会发起 MMA 后立即停下来等待它完成。它可以发起 MMA,开始准备后续 tile,并且只在真正需要结果时等待。代价是每个交接都必须显式表达。如果 epilogue 在 MMA completion barrier 触发前读取 TMEM,那就是读得太早。
## Accumulator 位于 TMEM
-TODO:翻译 “The Accumulator Lives in TMEM” 小节。
+在 Ampere 和 Hopper 上,accumulator 以寄存器形式暴露给程序。MMA 产生一个 per-lane register fragment,epilogue 直接消费这个 fragment。这很简单,但它把 accumulator 大小绑定到了每个线程的寄存器预算上。
+
+Blackwell 切断了这条绑定。`tcgen05.mma` 会把 accumulator 写入 TMEM,这是 Blackwell 上作用域属于 CTA 的内存空间。Accumulator 可以在 compute phase 中保存在 TMEM 中,epilogue 随后用 `tcgen05.ld` 把它加载回寄存器。
+
+这改变了 kernel 的形状。Register fragment 在边界上仍然重要。Epilogue 仍然需要寄存器,以便转换、执行 elementwise 工作并 store 结果。但长生命周期 accumulator state 不再是寄存器分配问题,而是 TMEM 分配和布局问题({ref}`chap_tmem`)。
+
+这就是为什么必须把 `tcgen05` 和 TMEM 放在一起理解。MMA 指令决定计算哪个 tile。TMEM 决定 accumulator 落在哪里。Epilogue 必须使用匹配的 load path,把 accumulator 恢复到它期望的 register layout。
## `cta_group::1` 和 `cta_group::2`
-TODO:翻译 “cta_group::1 and cta_group::2” 小节。
+`tcgen05` MMA 可以在 `cta_group::1` 或 `cta_group::2` mode 中运行。
+
+在 `cta_group::1` 中,一个 CTA 拥有这次 MMA。它的 operand 位于该 CTA 的 SMEM 中,accumulator 写入该 CTA 的 TMEM。
+
+在 `cta_group::2` 中,cluster 中的两个 CTA 协作执行一个 MMA tile。每个 CTA 都有自己的 SMEM 和自己的 TMEM。Accumulator 并不是存储在跨两个 CTA 的单个物理 TMEM 区域中。它会在两个 CTA 之间切分,每个 CTA 保存自己的部分。偶数 CTA 负责发起指令,并为这对 CTA commit completion barrier。
+
+这个选择很重要,因为它会改变逻辑 accumulator tile `C(M, N)` 到 TMEM 的映射。TMEM 有 128 个硬件 Lane 行和最多 512 个硬件 Col 列。在 TIRx 布局记号中,这些轴写作 `TLane` 和 `TCol`。MMA mode 决定 C 的行和列如何放到这些 TMEM 轴上。
+
+有四种有用情况需要记住。
+
+下面的图沿用 demo 中的颜色约定:紫色表示 SMEM operand,橙色表示 TMEM accumulator state,绿色表示 Tensor Core MMA 路径。CTA identity 通过标签和位置表示,而不是改变这些硬件颜色。
### `cta_group::1`,`M = 128`
-TODO:翻译本小节。
+这是最简单的情况。一个 CTA 计算一个 128-row tile。TMEM 也有 128 个 Lane 行。因此映射是直接的:accumulator 的 row `m` 映射到 Lane `m`,N 维度映射到 TMEM column。
+
+结果填满 128 个 Lane 行乘以 N 个 Col 列。这是基线图。CTA 在 SMEM 中拥有 A 和 B,并在自己的 TMEM 中拥有完整 accumulator tile。
-
+
### `cta_group::1`,`M = 64`
-TODO:翻译本小节。
+当 `M = 64` 时,accumulator 只有 64 行,但 TMEM 仍然有 128 个 Lane 行。硬件并不是简单地把 row 0 到 63 打包到 lane 0 到 63。相反,它会把这些行分散到四段 16-row run 中。
+
+Rows 0 到 15 放到 lanes 0 到 15。Rows 16 到 31 放到 lanes 32 到 47。Rows 32 到 47 放到 lanes 64 到 79。Rows 48 到 63 放到 lanes 96 到 111。
-
+这会在 lanes 16 到 31、48 到 63、80 到 95、112 到 127 留出空隙。这些空隙是有意的。使用不同 lane alignment 时,另一个独立的 `M = 64` MMA 可以占用互补 lane。这样两个较小的 M tile 可以共享 128-lane TMEM 结构,而不会互相覆盖。
+
+N 维度仍然映射到 TMEM column。不寻常的地方只有 M row 在 Lane 上的 placement。
+
+
### `cta_group::2`,`M = 256`
-TODO:翻译本小节。
+当 M 维度大到单个 CTA 自然容纳不下时,MMA 可以使用 `cta_group::2`。对于 `M = 256`,切分是直接的。CTA 0 持有 rows 0 到 127。CTA 1 持有 rows 128 到 255。
+
+每个 CTA 使用自己的 TMEM Lane rows 0 到 127,以及完整 N columns。物理上,这是两个独立的 128-row TMEM 区域,每个 CTA 一个。逻辑上,它们组成一个 256 by N 的 accumulator tile。
+
+每个 CTA 也提供 A 中对应自己 M row 的部分。B 会按照 mode 的要求对两个 CTA 可用。偶数 CTA 负责发起 MMA,并为这对 CTA commit completion barrier。
-
+这是 {ref}`chap_gemm_advanced` 中 two-CTA cluster GEMM 使用的 mode。
+
+
### `cta_group::2`,`M = 128`
-TODO:翻译本小节。
+`cta_group::2`, `M = 128` mode 仍然使用两个 CTA,但 M 维度更短。因为总共只有 128 行,每个 CTA 得到 64 个 M row。
+
+剩余 lane capacity 被用来打包 N 维度。在每个 CTA 内部,N 的一半占用 lanes 0 到 63,另一半占用 lanes 64 到 127。这样即使每个 CTA 只拥有 64 个 M row,也能使用全部 128 个 Lane 行。
+
+因此这个 split 有两部分。M 在 CTA pair 之间切分,每个 CTA 64 行。N 随后在每个 CTA 内部切分到 TMEM Lane 行的 lower half 和 upper half。
-
+
+
+在这些 mode 中,原则相同。`tcgen05.mma` 计算一个逻辑 accumulator tile,但这个 tile 必须放入物理的 128 Lane by up to 512 Col TMEM 空间中。Mode 和 M shape 决定这种 placement。Kernel 的其他部分稍后读取 accumulator 时,必须使用同一映射。
+
+对于这里的 kernel,accumulator 通常在 TMEM 中使用 f32。这是常见的高精度路径。它不是唯一可能的 accumulator type。`.kind::f16` 路径可以用 f16 accumulation。
## Operand Placement
-TODO:翻译 “Operand Placement” 小节。
+对于 dense MMA mode,A 和 B 在 MMA 运行前准备在 SMEM 中。TMA 负责把 global memory tile 移入 SMEM。Kernel 把这些 SMEM tile 安排成 Tensor Core 期望的布局,包括任何必要的 swizzle。
+
+Accumulator C 写入 TMEM。这是相对早期世代的主要区别。Epilogue 不会直接收到 MMA 指令输出的 accumulator。它必须用 `tcgen05.ld` 显式从 TMEM 加载。
+
+在 `cta_group::1` 中,一个 CTA 提供 operand 并拥有 accumulator。在 `cta_group::2` 中,每个 CTA 从自己的 SMEM 中提供自己那一侧的 operand,并拥有 accumulator 中自己的 TMEM 部分。当 A 按 M 切分时,每个 CTA 保留自己 M slice 对应的 A row。B 根据 mode 被共享,因为两个 M slice 都要乘同一个 N by K tile。
+
+阅读 kernel 时,这种分离很重要。SMEM placement 回答 Tensor Core 如何读取 A 和 B。TMEM placement 回答 accumulator 去哪里。这两个 layout 由 MMA mode 关联起来,但它们不是同一个内存空间,不能互换对待。
## Block-Scaled MMA
-TODO:翻译 “Block-Scaled MMA” 小节。
+Dense mode 直接从 SMEM 读取数据 operand,并累加到 TMEM。Block-scaled MMA 添加了两个额外 operand:A 和 B 的 scale-factor tensor。
+
+这用于 `mxfp8` 和 `nvfp4` 等非常低精度格式。低精度格式效率高,但动态范围小。单个 global scale 通常太粗。如果 scale 按最大值选择,小值会丢精度。如果 scale 按小值选择,大值可能溢出或被截断。
+
+Block scaling 通过给较小的 K block 分配 scale factor 来解决这个问题。一组连续 K element 共享一个 scale。MMA 在概念上先用 scale 对每个 block dequantize,再把乘积累加到 accumulator type 中。
+
+对于 A 和 B,这引入两个 scale-factor tensor:
+
+```text
+SFA(M, SFK)
+SFB(N, SFK)
+```
+
+其中 `SFK = K / B`,`B` 是沿 K 的 block size。
+
+具体 block size 取决于格式。重要的是,scale axis 以更粗粒度跟随 K。每个 scale factor 描述一块 K value,而不是单个元素,也不是整张矩阵。
+
+数学形状是:
+
+```text
+acc += (Aq * scale_a) * (Bq * scale_b)
+```
+
+其中 `Aq` 和 `Bq` 是量化的低精度值,scale 在 accumulation 前恢复它们的近似 magnitude。
+
+Scale dtype 也很重要。使用 `e8m0` scale 时,每个 scale 实际上是 2 的幂。使用 `e4m3` scale 时,例如 `nvfp4` 中的 scale,它是一个小浮点值,可以表示两个 2 的幂之间的数值。
## Scale Factor 放在哪里
-TODO:翻译 “Where the Scale Factors Live” 小节。
+Block-scaled `tcgen05.mma` 与 dense MMA 有一个重要 placement 规则不同:scale factor 从 TMEM 读取。
+
+数据 operand A 和 B 仍然 staged 在 SMEM 中。Scale factor SFA 和 SFB 通过 TMEM staged。由于 TMA 加载到 SMEM,scale factor 通常需要额外一步。Kernel 先把它们加载到 SMEM,再用 `tcgen05.cp` 从 SMEM copy 到 TMEM。只有 scale factor 位于 TMEM 后,block-scaled MMA 才能读取它们。
+
+这给 scale factor 一条不同于数据 operand 的移动路径:
+
+```text
+A, B: global memory to SMEM, then MMA reads SMEM
+SFA, SFB: global memory to SMEM, then tcgen05.cp copies SMEM to TMEM, then MMA reads TMEM
+```
+
+Scale factor 的 TMEM layout 是紧凑的。一个 128-row scale vector 可以打包到 32 个 Lane row 中,lane 位置基于 `r % 32` 映射,`r / 32` 沿 column 方向前进。随后数据可以 broadcast 到读取完整 128 Lane 空间的四个 warp 上({ref}`chap_layout_generations`)。
+
+这很好地说明为什么 TMEM layout 必须显式。Accumulator layout 和 scale-factor layout 都在 TMEM 中,但它们不是同一个 layout。Accumulator 使用 MMA output mapping。Scale factor 使用 block-scaled MMA 期望的紧凑布局。
## `cta_group::2` 中的 Scale Factor
-TODO:翻译 “Scale Factors in cta_group::2” 小节。
+在 two-CTA 情况下,scale factor 跟随它们缩放的数据。
+
+SFA 缩放 A。由于 A 在 CTA pair 之间按 M 切分,SFA 也按 M 切分。每个 CTA 持有与自己 A row 对应的 SFA row。
+
+SFB 缩放 B。由于两个 CTA 都要乘同一个 B tile,SFB 必须对两个 CTA 可见。实践中,这意味着 SFB 会 multicast 到 CTA pair。
-
+这是 block-scaled cluster GEMM 中常见 loading pattern 的来源。SFA 按 CTA 加载,使用该 CTA 自己 M slice 的 mask。SFB broadcast 给这对 CTA,因为两个 CTA 都需要同一组 N-side scale factor。
+
+
## 保持 MMA Contract 匹配
-TODO:翻译 “Keeping the MMA Contracts Matched” 小节。
+一个 Blackwell GEMM tile 会经过多条专用路径。
+
+TMA 把 A 和 B 从 global memory 带入 SMEM。对于 block-scaled mode,它也会把 scale factor 带入 SMEM。需要时,`tcgen05.cp` 把这些 scale factor 移到 TMEM。`tcgen05.mma` 读取 operand,在 Tensor Core 上异步运行,并累加到 TMEM。Completion barrier 告诉 kernel accumulator 何时就绪。Epilogue 随后用 `tcgen05.ld` 从 TMEM 把 accumulator 加载回寄存器,并 store 最终输出。
+
+在这些路径之间,kernel 必须保持三份 contract 匹配:SMEM operand layout、TMEM accumulator 或 scale-factor layout,以及让下一个 consumer 安全运行的异步完成信号。
diff --git a/zh/chapter_tirx_layout_api/index.md b/zh/chapter_tirx_layout_api/index.md
index 2c7388cc..e559987e 100644
--- a/zh/chapter_tirx_layout_api/index.md
+++ b/zh/chapter_tirx_layout_api/index.md
@@ -1,10 +1,711 @@
----
-orphan: true
----
-
(chap_tirx_layout_api)=
# TIRx Layout API
-> 翻译状态:待翻译。对应英文章节:`chapter_tirx_layout_api/index.md`。
+:::{admonition} 概览
+:class: overview
+
+- TIRx layout API 会把 {ref}`chap_data_layout` 中的布局记号变成编译器对象。主要对象是 `TileLayout`、`SwizzleLayout` 和 `ComposeLayout`。
+- `TileLayout` 描述命名硬件轴上的 affine placement。它由 shard spec `S[...]`、replica spec `R[...]` 和可选 offset 构成。
+- 一个布局会把一个逻辑坐标映射到一个或多个物理坐标。`layout.apply()` 会计算这个映射。
+- `SwizzleLayout` 描述用于避免 bank conflict 的 XOR-based shared-memory swizzle。`ComposeLayout` 会把一个 swizzle 叠加到 tile layout 上。
+- `tmem_datapath_layout`、`tcgen05_atom_layout` 和 `wg_local_layout` 这类现成 constructor 覆盖了 kernel 中反复出现的硬件布局。
+:::
+
+{ref}`chap_data_layout` 介绍了本书一直使用的记号:tile shape、命名轴上的一组 stride,以及可选的 replication term,用于表示被复制而不是被切分的值。本章会把这套记号变成编译器使用的 API。
+
+目标是让页面上的记号和 kernel 里的代码几乎长得一样。当你写出这样的布局时:
+
+```python
+S[(128, 256) : (1@TLane, 1@TCol)]
+```
+
+你写的不只是解释文字。你正在构造一个可以挂到 buffer 上的 `TileLayout` 对象。之后,每个触碰这个 buffer 的 tile operation 都能从 layout 中读取它的 placement。Placement 只写一次、检查一次,并由编译器复用。
+
+布局会在从 pool 分配时,或声明 buffer 时附着上去:
+
+```python
+pool.alloc(shape, dtype, layout=layout)
+
+T.decl_buffer(shape, dtype, scope=scope, layout=layout)
+```
+
+从那时起,buffer 就携带了自己的物理 placement。Tile operation 不需要反复说明每个元素位于哪里。
+
+这些 layout object 位于同一个模块中:
+
+```python
+from tvm.tirx.layout import (
+ TileLayout,
+ SwizzleLayout,
+ ComposeLayout,
+ S,
+ R,
+ laneid,
+ warpid,
+ tid_in_wg,
+ TLane,
+ TCol,
+ m,
+ tcgen05_atom_layout,
+ tmem_datapath_layout,
+)
+```
+
+这个 API 背后有一个核心思想:布局不一定把逻辑索引映射到单个物理地址。它会把逻辑索引映射到命名轴上的一组物理坐标。通常这组坐标只有一个元素。当存在 replication 时,同一个逻辑元素会有多个物理 placement。
+
+这就是为什么 layout model 有三部分:shard、replica 和 offset。Shard 放置元素。Replica 把它复制到额外坐标。Offset 移动整个 placement。
+
+## 通过例子看布局
+
+下面的例子展示 API 的基本形状。
+
+TMEM 中的 accumulator 可以写成覆盖 TMEM 轴的直接 placement:
+
+```python
+acc = TileLayout(S[(128, 256) : (1@TLane, 1@TCol)])
+```
+
+这里逻辑行映射到 `TLane`,逻辑列映射到 `TCol`。在 {ref}`chap_tmem` 中,硬件坐标称为 Lane 和 Col。在 TIRx 布局记号中,这些硬件轴写作 `TLane` 和 `TCol`。
+
+Block-scaled MMA 的 scale-factor layout 会使用 replication:
+
+```python
+scale_factor_layout = TileLayout(
+ S[(32, sf_per_mma) : (1@TLane, 1@TCol)] + R[4 : 32@TLane]
+)
+```
+
+Shard 会把一个 32-row group 放入 TMEM。Replica 以 32 个 lane 为 stride,把这个 group 重复四次,使这个 32-row group 在完整 128-lane TMEM 空间中都可见。
+
+Tensor-core register fragment 可以分布在 lane 和 warp 上:
+
+```python
+frag = TileLayout(
+ S[(8, 2, 4, 2) : (4@laneid, 1@warpid, 1@laneid, 1)]
+)
+```
+
+同一个物理轴可以出现多次。在这个例子中,两个不同 iter 都会贡献到 `laneid`。没有显式 axis 的 stride 使用默认 memory axis `m`。
+
+在真实 kernel 中,常见硬件布局通常来自 constructor:
+
+```python
+acc = tmem_datapath_layout("D", 128, 256)
+
+ld = tcgen05_atom_layout("32x32b", (128, 64), "float32")
+```
+
+这些 constructor 返回普通 `TileLayout` 对象。它们只是方便写法,不是单独机制。你可以检查返回的 layout,把它和其他 layout 组合,或者在 shape 不寻常时手写底层 `S[...]` 和 `R[...]` 形式。
+
+## 交互 Demo
+
+在进入机制之前,先有一个可以操作的具体对象会更容易理解。下面的 demo 允许你选择 preset layout、编辑 logical shape 和 `S` 或 `R` term、选择 dtype 和 swizzle mode,并点击某个 element 查看哪个物理坐标或哪些物理坐标拥有它。
+
+```{raw} html
+
+ ▶ 全屏打开 demo ↗
+
+
+
+```
+
+这个 demo 很有用,因为 API 的大部分内容只是把 demo 中展示的过程精确定义出来。一个 logical element 进入 layout。Layout 会把它 flatten,按 iter 拆分,在命名轴上累加坐标,然后在需要时应用 replication。
+
+## TileLayout
+
+`TileLayout` 是主要的 affine layout object。它通常用正文中同样的记号写出:
+
+```python
+TileLayout(S[shape : strides])
+```
+
+`S` term 是 shard spec。可以这样读:取一个具有这个 shape 的逻辑 tile,并用这些命名轴 stride 放置它。
+
+当某个值需要出现在多个地方时,shard spec 会扩展出 replica spec:
+
+```python
+TileLayout(S[shape : strides] + R[replica_shape : replica_stride])
+```
+
+还可以加入可选 offset:
+
+```python
+TileLayout(S[shape : strides] + R[replica_shape : replica_stride] + offset)
+```
+
+在表面之下,这些部分由 iter 表示。一个 iter 是三元组:
+
+```text
+(extent, stride, axis)
+```
+
+它描述命名轴上的一次 strided walk。Extent 表示这个 iter 有多少个位置。Stride 表示每一步移动多远。Axis 表示被改变的是哪个硬件坐标。
+
+一个 layout 有三部分。
+
+### Shard
+
+Shard,也就是 `D`,是由 `S[...]` 构建的部分。它把逻辑索引切分到一个或多个 iter 上,并产生 base physical coordinate。
+
+例如:
+
+```python
+S[(8, 2, 4, 2) : (4@laneid, 1@warpid, 1@laneid, 1)]
+```
+
+有四个 shard iter。它们的 extent 是 `8`、`2`、`4` 和 `2`。它们的 stride 会把数据放到 `laneid`、`warpid`、再次 `laneid`,以及默认 memory axis `m` 上。
+
+这推广了普通 shape-and-stride 规则。差别在于,这里的 stride 附着在命名硬件轴上,而不是单个 flat address 上。
+
+### Replica
+
+Replica,也就是 `R`,描述同一个逻辑元素的额外物理副本。Replica iter 与逻辑索引无关。它们枚举硬件空间中的额外 offset。
+
+例如:
+
+```python
+R[2 : 4@warpid]
+```
+
+会在 `warpid` 轴上创建两个相距四个 warp 的副本。
+
+Replication 不是为了方便写法的技巧。它描述真实硬件行为。有些数据会 broadcast 到多个 warp、lane 或 memory region。Logical-to-physical mapping 很自然地支持这一点,因为一个逻辑元素可以映射到一组物理坐标。
+
+### Offset
+
+Offset,也就是 `O`,是加到每个结果上的固定坐标。
+
+例如:
+
+```python
+5@warpid
+```
+
+会把整个 placement 在 `warpid` 轴上移动 5。
+
+Offset 用于把 tile 放到选定 base coordinate、为独占使用预留区域,或者描述在同一资源中位于另一个 tile 之后开始的 tile。
+
+### 把三部分合在一起
+
+Layout 会按顺序应用这三部分。
+
+首先,shard 计算 base coordinate。然后,replica 把这个 coordinate fan out 成零个或多个额外副本。最后,offset 移动每个 coordinate。
+
+对于逻辑坐标 `x`,结果是:
+
+```text
+L(x) = { D(x) + r + O | r in R }
+```
+
+如果没有 replica,`R` 只包含 zero offset,因此结果是 singleton set。如果有 replica,结果中会有每个 replica position 对应的一个 coordinate。
+
+在 TIRx 语法中,一个完整 layout 可以写作:
+
+```python
+layout = TileLayout(
+ S[(8, 2, 4, 2) : (4@laneid, 1@warpid, 1@laneid, 1)]
+ + R[2 : 4@warpid]
+ + 5@warpid
+)
+```
+
+从左到右读,shard 放置逻辑 tile,replica 在四个 warp ID 之外创建第二份副本,offset 把整个 placement 移动到从 `warpid = 5` 开始。
+
+如果 iter 已经被构造成对象,同一个 layout 也可以直接构造:
+
+```python
+TileLayout.from_iters(shard, replica, offset)
+```
+
+大多数用户代码使用 `S[...]` 和 `R[...]` 记号,因为它更接近数学形式。
+
+## 命名轴
+
+Layout 中的轴不是匿名维度。每个轴都命名一个真实硬件坐标,或者一个编译器级 placement 坐标。
+
+例子包括:
+
+```text
+bx, by, bz
+cbx, cby, cbz
+tx
+warpid
+laneid
+wgid
+tid_in_wg
+wid_in_wg
+m
+P, F
+Bank
+TLane, TCol
+```
+
+`bx`、`by`、`bz` 这样的 grid axis 把 work 放到 CTA 上。`cbx`、`cby`、`cbz` 这样的 cluster axis 把 work 放到 CTA cluster 内部。`tx`、`warpid`、`laneid`、`tid_in_wg` 和 `wid_in_wg` 这样的 thread axis 描述 CTA 或 warpgroup 内的 ownership。`m` 是默认 linear memory axis。`P` 和 `F` 用于二维 scratchpad-style placement。`Bank` 命名 shared memory bank。`TLane` 和 `TCol` 是 TIRx 布局中对 TMEM Lane 和 Col 坐标的命名。
+
+Axis name 是 layout 的一部分。这一点很重要,因为两个整数值相同的坐标可能表示不同硬件事物。`1@tx` 不是 `1@tid_in_wg`。`1@laneid` 不是 `1@TLane`。Layout 会让这些含义保持显式。
+
+## Forward Mapping
+
+求值一个 layout,意味着取一个逻辑坐标并计算它物理上落在哪里。API 方法是:
+
+```python
+layout.apply(*coord)
+```
+
+对于没有 replication 的 layout,结果是一个 coordinate dictionary。带有 replication 时,结果是一组 coordinate dictionary。Coordinate dictionary 把 axis name 映射到整数位置,例如:
+
+```python
+{"laneid": 7, "warpid": 2, "m": 1}
+```
+
+求值规则有四步。
+
+第一步,按 row-major 顺序 flatten 逻辑坐标。对于逻辑 shape:
+
+```text
+(S0, S1, ..., Sr-1)
+```
+
+中的逻辑坐标:
+
+```text
+x = (x0, x1, ..., xr-1)
+```
+
+flat index 是:
+
+```text
+flat = x0 * S1 * S2 * ... * Sr-1
+ + x1 * S2 * ... * Sr-1
+ + ...
+ + xr-2 * Sr-1
+ + xr-1
+```
+
+第二步,把这个 flat index 按 shard extents 拆分。如果 shard extents 是:
+
+```text
+(e0, e1, ..., en-1)
+```
+
+那么拆分会产生 components:
+
+```text
+c0, c1, ..., cn-1
+```
+
+使用的是同样的 row-major 顺序,只不过作用在 shard extents 上。
+
+第三步,用每个 component 的 stride 把它累加到对应 axis 上。如果 shard iter `k` 的 extent 是 `ek`、stride 是 `sk`、axis 是 `ak`,那么 component `ck` 的贡献是:
+
+```text
+ck * sk @ ak
+```
+
+对同一个 axis 的所有贡献会相加。然后加入 offset。
+
+第四步,应用 replica iter。每个 replica iter 都贡献一个与逻辑坐标无关的额外 offset。如果有多个 replica iter,layout 会枚举所有组合。
+
+这个规则有一个有用结果:layout 不需要 hard-code 输入 shape。它需要的是逻辑 tile 的元素总数等于 shard extents 的乘积。只要这个条件成立,flattening 和 splitting 就定义了映射。
+
+## Case Study:Tensor Core Register Tile
+
+考虑一个逻辑 `(8, 16)` tile,它分布在两个 warp 上,每个 warp 有 32 个 lane。每个 lane 拥有一个小 register fragment。Register slot 用默认 memory axis `m` 表示。
+
+```python
+layout = TileLayout(
+ S[(8, 2, 4, 2) : (4@laneid, 1@warpid, 1@laneid, 1)]
+ + R[2 : 4@warpid]
+ + 5@warpid
+)
+```
+
+取 `(8, 16)` tile 中的一个逻辑元素 `(i, j)`。
+
+Row-major flat index 是:
+
+```text
+flat = 16 * i + j
+```
+
+按 shard extents `(8, 2, 4, 2)` 拆分得到:
+
+```text
+c0 = i
+c1 = floor(j / 8)
+c2 = floor(j / 2) mod 4
+c3 = j mod 2
+```
+
+Shard 贡献是:
+
+```text
+laneid = 4 * c0 + c2
+warpid = c1
+m = c3
+```
+
+加上 offset `5@warpid` 后变成:
+
+```text
+laneid = 4 * i + floor(j / 2) mod 4
+warpid = floor(j / 8) + 5
+m = j mod 2
+```
+
+Replica term:
+
+```python
+R[2 : 4@warpid]
+```
+
+会向 `warpid` 添加 `0` 或 `4`。因此完整映射是:
+
+```text
+laneid = 4 * i + floor(j / 2) mod 4
+warpid = floor(j / 8) + 5 + 4 * r, where r in {0, 1}
+m = j mod 2
+```
+
+Shard 把 tile 放到 warps 5 和 6 上。Replica 随后把它复制到 warps 9 和 10。于是同一个逻辑元素会出现在两个 warp 位置。
+
+这个例子说明了为什么模型使用一组物理坐标。Replication 很难自然表示成从物理坐标到逻辑坐标的函数。它更自然地表示成从一个逻辑坐标到多个物理坐标的函数。
+
+## Case Study:Blackwell Tensor Memory
+
+同一个 layout model 也适用于内存 placement。Axis 不一定是 thread axis,也可以是 memory axis。
+
+TMEM 通过硬件 Lane 和 Col 坐标寻址。在 TIRx 布局记号中,这些轴写作 `TLane` 和 `TCol`。
+
+考虑这个 layout:
+
+```python
+layout = TileLayout(
+ S[(2, 128, 112) : (112@TCol, 1@TLane, 1@TCol)]
+)
+```
+
+如果逻辑 tile shape 是 `(2, 128, 112)`,split component 就是逻辑坐标本身。对于元素 `(a, l, c)`,映射是:
+
+```text
+TLane = l
+TCol = 112 * a + c
+```
+
+Extent-128 iter 以 stride `1@TLane` 填充 128 个 TMEM Lane row。Extent-2 iter 以 stride `112@TCol`,extent-112 iter 以 stride `1@TCol`,两者共同覆盖 224 列:
+
+```text
+TCol in [0, 224)
+```
+
+224-column span 是有意选择的。TMEM layout 不一定是 2 的幂。Block-scaled FP8 GEMM 可能选择 224-column accumulator,因为完整 256-column tile 可能无法为两个 accumulator stage 加 scale factor 留出足够 TMEM 容量。Layout API 可以直接表达这种 shape。
+
+## Scale Factor Layout
+
+上面的 accumulator layout 是纯 placement。每个逻辑 accumulator element 映射到一个 TMEM coordinate。Block-scaled MMA 的 scale factor 不同,因为同一个物理 group 可能需要在多个 warp window 中可见。这正是 replication 有用的地方。
+
+一个紧凑 scale-factor layout 可以写作:
+
+```python
+scale = TileLayout(
+ S[(32, sf_per_mma) : (1@TLane, 1@TCol)]
+ + R[4 : 32@TLane]
+)
+```
+
+Shard 把一个 32-row scale-factor group 放入 TMEM:
+
+```text
+TLane = r
+TCol = s
+```
+
+对于逻辑 scale coordinate `(r, s)`。
+
+Replica term 会创建四份副本,间隔 32 个 lane:
+
+```text
+TLane = r + 32 * q, where q in {0, 1, 2, 3}
+TCol = s
+```
+
+所以 32-row group 会在 TMEM lanes 0 到 31、32 到 63、64 到 95 和 96 到 127 处可见。这就是 `warpx4` broadcast pattern({ref}`chap_layout_generations`)。四个 warp-sized TMEM lane window 中的每一个,都能看到同一个 scale-factor group。
+
+在完整 block-scaled MMA layout 中,这个 atom 会与 M row 和 K scale-factor group 上的 outer iter 组合在一起。多个 scale factor 也可能被打包到同一个 32-bit `TCol` cell 中,具体取决于 scale-factor dtype。例如,fp8 scale factor 可以把四个值打包到一个 32-bit column cell 中。可选的 stride-zero reuse 和 pipeline-depth iter 可以进一步描述多个 MMA 之间的 scale reuse 以及 double buffering。
+
+重要的是,同一个 `TileLayout` model 描述了两种情况。Accumulator 是 TMEM 中的单一 placement。Scale factor 是同一 TMEM 地址空间中的 replicated placement。
+
+## 现成 Layout
+
+大多数 kernel 不会手写每一个硬件布局。TIRx 为常见布局提供了 constructor。
+
+```python
+tmem_datapath_layout(datapath, rows, cols)
+```
+
+返回 `tcgen05.mma` 写出的 TMEM accumulator layout。`datapath` 参数选择 row placement pattern。例如,`"D"` 对应 `M = 128` identity-style placement,而 `"F"` 对应 `M = 64` scattered placement。
+
+```python
+tcgen05_atom_layout(instr_shape, tensor_shape, dtype)
+```
+
+返回由 `tcgen05.ld` 或 `tcgen05.st` atom 移动的 register tile layout。Instruction shape 的例子包括 `.32x32b`、`.16x64b`、`.16x128b` 以及相关形式。在 DSL 层面,这是一个 warpgroup-distributed tile。Lowering 时,它会变成四条 warp-collective `tcgen05.ld` 或 `tcgen05.st` 指令,每个 warp 一条,并且每个 warp 处理自己的 32 个 TMEM lane。
+
+```python
+wg_local_layout(cols, rows=128)
+```
+
+返回一个 warpgroup-local register tile,通常在 `tid_in_wg` 上每个线程对应一行。
+
+这些 helper 的作用是避免手写常见硬件映射。它们不会隐藏模型。每个 helper 都返回一个普通 `TileLayout`,由上面描述的同一组 `S` 和 `R` 片段构成。
+
+## SwizzleLayout 和 ComposeLayout
+
+`TileLayout` 是 affine 的。它可以在命名轴上表达 stride、replication 和 offset。这足以描述很多 placement,包括 thread fragment、TMEM tile 和紧凑 scale-factor layout。
+
+Shared memory swizzle 需要另一种东西。用于避免 bank conflict 的 swizzle 不是 affine stride pattern。它是对线性 shared-memory address 的 XOR-based permutation。
+
+因此,TIRx 把 swizzling 保持为一个单独的 layout object:
+
+```python
+SwizzleLayout(...)
+```
+
+并把它与 tile layout 组合:
+
+```python
+ComposeLayout(swizzle, tile)
+```
+
+Tile layout 先产生线性 memory address。Swizzle 随后重排这个 address。把两层分开,比强行把 XOR permutation 塞进 affine layout model 更干净。
+
+## 为什么需要 Swizzle
+
+Shared memory 分成 32 个 bank,每个 bank word 保存 4 byte。当一次访问中的多个 lane 触碰同一 bank 中的不同地址时,访问会因为 bank conflict 被串行化。
+
+普通 row-major tile 会结构性地产生这种 conflict。考虑一个 row-major layout 的 `(8, 64)` float16 tile:
+
+```python
+TileLayout(S[(8, 64) : (64@m, 1@m)])
+```
+
+逻辑元素 `(i, j)` 的线性 element address 是:
+
+```text
+m = 64 * i + j
+```
+
+每行有 64 个 float16 值,也就是 128 byte。这刚好是一整条 shared memory bank line。如果一个 warp 沿固定 `j` 读取 column,每向下一行都会前进完整 128-byte line。Bank index 重复,因此 column read 会跨行塌缩到同一个 bank 上。
+
+Swizzle 通过让低地址位依赖更高的 row bit 来改变这一点。原本会反复落到同一个 bank 的 column 会被分散到不同 bank 上。
+
+## Swizzle Transform
+
+`SwizzleLayout` 由三个整数参数控制:
+
+```text
+per_element = M
+swizzle_len = B
+atom_len = S
+```
+
+输入是一个线性 element address `m`。
+
+`m` 的低 `M` 位保持不变。这会保留一个小的连续 element group。更高的 bit 会被右移到一个临时值中:
+
+```text
+x = m >> M
+```
+
+然后,`x` 中位于 `[S, S + B)` 的 bit group 会 XOR 到 `x` 中位于 `[0, B)` 的 bit group 上。最后把保持不变的低 `M` bit 放回去,形成 swizzled address。
+
+等价地:
+
+```text
+mask = (1 << B) - 1
+
+low = m & ((1 << M) - 1)
+x = m >> M
+x2 = x ^ ((x >> S) & mask)
+
+addr = (x2 << M) | low
+```
+
+要让 layout well formed,`S` 必须至少为 `B`。
+
+这个 transform 的目的不是改变 tile 中有哪些逻辑元素。它改变的是这些元素落在 shared memory 中的位置。MMA 仍然读取同一个逻辑 tile。Swizzle 让物理 bank pattern 更好。
+
+## 选择 Swizzle 参数
+
+正常使用时,swizzle 参数由 dtype 和 shared-memory swizzle mode 共同决定。常见 mode 是 32-byte、64-byte 和 128-byte swizzle。
+
+`per_element` 参数的选择要保证一个小的 vector-sized group 保持连续。对于 float16,一个 16-byte vector 包含 8 个元素,因此:
+
+```text
+M = log2(8) = 3
+```
+
+使用 128-byte swizzle 时,layout 使用:
+
+```python
+SwizzleLayout(per_element=3, swizzle_len=3, atom_len=3)
+```
+
+这会保持 16-byte vector group 完整,同时仍然充分重排更大的 shared-memory address pattern,以打破 column bank conflict。
+
+大多数代码不应该手工推导这些参数。Dtype 和 descriptor mode 通常会决定它们。对程序员来说,重要的是 TIRx layout 中的 swizzle、TMA descriptor 和 MMA 期望三者保持匹配。
+
+一个 swizzled shared memory allocation 因此会写成:
+
+```python
+tile = TileLayout(S[(8, 64) : (64@m, 1@m)])
+swizzle = SwizzleLayout(per_element=3, swizzle_len=3, atom_len=3)
+
+layout = ComposeLayout(swizzle, tile)
+```
+
+组合后的 layout 会附着到 shared memory buffer 上。
+
+## 元素的 Bank 和 Line
+
+要判断 swizzle 是否有帮助,可以把 swizzled element address 转回 shared memory bank。
+
+设 `addr` 为 swizzled element address,`b` 为 element size in bytes。Byte address 是:
+
+```text
+byte = addr * b
+```
+
+Bank 是:
+
+```text
+bank = floor(byte / 4) mod 32
+```
+
+128-byte bank line 是:
+
+```text
+line = floor(byte / 128)
+```
+
+对于 float16,`b = 2`,因此 bank 公式变成:
+
+```text
+bank = floor(addr / 2) mod 32
+```
+
+这就是下面 worked example 使用的公式。
+
+## Worked Example:`(8, 64)` float16 Tile 上的 128B Swizzle
+
+回到 row-major float16 tile:
+
+```text
+m = 64 * i + j
+```
+
+使用:
+
+```python
+SwizzleLayout(per_element=3, swizzle_len=3, atom_len=3)
+```
+
+Transform 变成:
+
+```text
+x = m >> 3
+addr = ((x ^ ((x >> 3) & 7)) << 3) | (m & 7)
+```
+
+因为:
+
+```text
+m = 64 * i + j
+```
+
+我们可以写:
+
+```text
+q = floor(j / 8)
+r = j mod 8
+```
+
+swizzled address 是:
+
+```text
+addr = 64 * i + 8 * (q xor i) + r
+```
+
+现在看 column `j = 0`。此时 `q = 0` 且 `r = 0`,因此:
+
+```text
+addr = 72 * i
+```
+
+对于 float16,bank 是:
+
+```text
+bank = floor(addr / 2) mod 32
+```
+
+所以八行映射到:
+
+```text
+i = 0: bank 0
+i = 1: bank 4
+i = 2: bank 8
+i = 3: bank 12
+i = 4: bank 16
+i = 5: bank 20
+i = 6: bank 24
+i = 7: bank 28
+```
+
+这个 column 现在触碰八个不同 bank。Conflict 消失了。
+
+如果没有 swizzling,同一个 column 的地址是:
+
+```text
+m = 64 * i
+```
+
+因此:
+
+```text
+bank = floor(64 * i / 2) mod 32 = 0
+```
+
+每一行都落在 bank 0 上,所以访问会被串行化。Swizzle 只改变物理 placement,但这已经足以把 column access 变成 conflict-free。
+
+这个保证依赖于按设计方式使用 swizzle。Dtype、swizzle width 和 access shape 必须匹配 TMA 和 MMA descriptor mode。128-byte float16 swizzle 围绕相关的 16-byte row chunk 和 Tensor Core access pattern 设计。它并不承诺任意 shared memory access 都会变成 conflict free。本章开头的 demo 可以看到这一点:选择 dtype 和 swizzle mode,观察无 swizzle 时一个 column 如何塌缩到一个 bank 上,再观察匹配 swizzle 应用后它如何分散到 bank view 中。
+
+## 设计理由
+
+Layout API 遵循三个设计选择。
+
+第一,它支持通用 shape。硬件 tile 并不总是 2 的幂。Global tensor、shared memory stage、TMEM accumulator 和 scale-factor buffer 的 shape 常常来自容量限制或算法选择。Layout model 把这些 shape 当作正常情况处理。
+
+第二,映射方向是从逻辑坐标到物理坐标。这个方向很重要,因为 replication 很常见。一个逻辑元素可能位于多个物理位置。Logical-to-physical map 可以直接把它表示成一组坐标。
+
+第三,硬件轴是显式的。Layout 不使用匿名维度,再依赖上下文在事后解释它们。`tx`、`tid_in_wg`、`laneid`、`warpid`、`TLane` 和 `TCol` 之间的差异直接写在 layout 里。
-本页用于放置 TIRx layout API 相关内容的中文翻译。
+Legality 和 feasibility 检查不只由 layout object 自己负责。Layout 可以说明数据放在哪里。更高层的 tile primitive 会决定某个操作能否合法且高效地使用这个 placement。这种分离让 layout API 保持小而清晰,同时仍然给编译器足够信息去 dispatch 真实硬件操作。
diff --git a/zh/chapter_tma/index.md b/zh/chapter_tma/index.md
index f45a98ab..d45eca45 100644
--- a/zh/chapter_tma/index.md
+++ b/zh/chapter_tma/index.md
@@ -1,66 +1,147 @@
(chap_tma)=
# 异步数据搬运:TMA
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- TMA 是在 global memory 和 shared memory 之间异步拷贝 tile 的硬件引擎。一个线程发起 copy,硬件引擎移动 byte。
+- 一次 TMA copy 由 tensor-map descriptor 描述。Descriptor 告诉引擎 global tensor shape、stride、tile coordinate,以及 shared-memory swizzle mode。
+- 在 load 路径上,TMA 可以在写 shared memory 时对 tile 应用 swizzle,使 tile 直接落到 Tensor Core 期望的布局中。
+- TMA load 通过带 byte-count tracking 的 `mbarrier` 完成。TMA store 使用 commit group 和 wait group。
:::
-TODO:翻译导言部分。
+只有当数据已经准备好供 Tensor Core 消费时,Tensor Core 才有用。在 GEMM 或 attention kernel 中,一旦 pipeline 被填满,数学部分可能是 compute-bound({ref}`chap_performance`),但只有下一块 operand tile 及时到达,pipeline 才能一直保持填满。
+
+移动 tile 的旧方式是让线程自己 copy。每个线程计算地址,从 global memory 发起 load,再把值 store 到 shared memory。这样当然可行,但它把 warp 指令花在地址计算和 copy bookkeeping 上,而不是计算上。它也让 copy 路径暴露在同一批本应喂给 Tensor Core 的 warp 的指令流中。
+
+Tensor Memory Accelerator,简称 TMA,会把这项工作移动到硬件 copy engine 中。一个线程发起一次 tile copy,copy engine 随后在 global memory 和 shared memory 之间异步移动一个矩形 tile。当引擎移动 byte 时,CTA 的其他部分可以继续执行别的工作。
+
+TMA 还会处理一部分布局问题。Tensor Core 不只是需要 shared memory 中有正确的值。它还需要这些值位于正确的 shared-memory layout 中。在 load 路径上,TMA 可以在写 tile 时应用 shared-memory swizzle。这样 tile 会直接落到后续 MMA 期望的布局中。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
```
-
-*点击图中组件查看细节:TODO:翻译 TMA intro 图注。*
+*交互图:TMA 把 tile 从 global memory 拷贝到 shared memory。切换 swizzle mode,并悬停在 source cell 上查看它在 shared memory 中落到哪里。*
## 一个线程发起,硬件搬运 Tile
-TODO:翻译 “One Thread Issues, Hardware Moves the Tile” 小节。
+一次 TMA copy 从一个 issuing thread 开始。这个线程不会遍历 tile 中的所有元素。它会把 copy 的描述交给硬件,然后由 TMA engine 执行整个 transfer。
+
+主要输入是 tensor-map descriptor。Descriptor 描述 global tensor,以及应该如何从中读取一个 tile。它会记录 tensor shape、stride、element size、tile shape 和 swizzle mode 等信息。Issuing thread 还会提供 tile 应该落到的 shared-memory address。
+
+指令发出之后,copy 会异步运行。Issuing thread 可以继续执行。CTA 中的其他线程也可以继续执行。Transfer 现在由 TMA engine 负责,而不是由普通 load/store 指令组成的 loop 负责。
+
+这让 kernel 有两种不同方式表达同一个逻辑操作:“copy 这个 tile”。
+
+一条路径是 thread copy。线程协作从 global memory load,并 store 到 shared memory。这样 kernel 可以直接控制每一次访问,但会消耗线程指令和寄存器来做地址计算。
+
+另一条路径是 TMA copy。一个线程发起 transfer,硬件 copy engine 执行矩形 copy。对于大的规则 tile,尤其是 Tensor Core kernel 使用的 operand tile,这是自然路径。
+
+这两条路径有不同的同步规则和不同的性能行为。选择其中哪一条是一个 dispatch decision。Layout 告诉 kernel 想要什么内存排列。Scope 告诉它哪些线程或 CTA 参与。Dispatch 决定这次 copy 是由普通 thread copy 实现,还是由 TMA 实现。
## Swizzled Layout
-TODO:翻译 “Swizzled Layouts” 小节。
+移动 tile 本身还不够。Tile 还必须以 Tensor Core 能高效读取的布局放入 shared memory。
+
+这就是 TMA swizzling 的用途。当 TMA 把 tile 写入 shared memory 时,它可以重排 shared-memory address pattern。Global memory tile 仍然是一个逻辑矩形,但 shared memory 中的 destination layout 可以是 swizzled 的。
+
+Swizzle mode 是 TMA descriptor 的一部分。Descriptor 设置好之后,issuing thread 不需要手工应用 swizzle。Engine 会在 byte 落入 shared memory 时应用它。
+
+重要要求是一致性。TMA descriptor、shared-memory tile layout,以及后续 MMA 指令,都必须描述同一个布局({ref}`chap_data_layout`)。如果 TMA 用一种 swizzle 写入 tile,而 MMA 以为它是另一种 swizzle,硬件仍然会忠实执行收到的指令。只是这些 byte 对计算来说会排列错误。
+
+这正是布局记号不再只是 bookkeeping 的地方。DSL 使用的布局必须匹配 TMA descriptor 和 Tensor Core 指令使用的硬件布局。例如,如果 kernel 说某个 operand tile 存储在 128-byte swizzled layout 中,TMA descriptor 就必须使用匹配的 swizzle mode,MMA dispatch 也必须期望同样的 shared-memory arrangement。上面的 demo 可以在 no swizzle 和 128-byte swizzle 之间切换;悬停在 source element 上可以看到 swizzle 应用后它落在哪里。
+
+理解 swizzle 的一个有用方式是:TMA 并没有改变逻辑 tile。它改变的是逻辑元素在 shared memory 中的物理落点。后续 MMA 仍然消费同一个逻辑 A 或 B tile。Swizzle 只决定这个 tile 如何排列在 shared memory bank 上。
## 用 3D TMA 表达 Tiling 和 Swizzling
-TODO:翻译 “3D TMA for Tiling and Swizzling” 小节。
+普通 TMA copy 会移动一个扁平 2D tile,但 Tensor Core 期望的 shared-memory layout 通常会被 *tiled* 成 swizzle atom(来自 {ref}`chap_data_layout` 的 8 x 128-byte atom)。TMA 通过额外的 descriptor dimension 处理这一点。**3D TMA** 把 shared-memory box 描述成 `(group, row, col)`,其中 group 维度沿 atom 前进,内部两个维度在一个 atom 内寻址。一次 3D copy 随后既会 atom by atom 地布置 tile(tiling),又会在每个 atom 内应用 swizzle,因此数据到达时就已经在 MMA 期望的布局中,不需要单独的 tiling 或 swizzling pass。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
```
+*交互图:一次 3D TMA copy,以 (group, row, col) 寻址,并 tiled 到 swizzled shared memory 中。*
-*点击图中组件查看细节:TODO:翻译 3D TMA 图注。*
+选择 swizzle *format* 与这种 tiling 绑定在一起。更宽的 swizzle 会把一个 column 分散到更多 bank,所以只要能适用,128-byte swizzle 就是默认选择。但一个 N-byte atom 要求 tile 的 contiguous dimension 能填满它。因此,一个由于 shape constraint 而变小的 tile 不能使用 128-byte swizzle,必须降到 64-byte 或 32-byte:经验法则是选择 tile 能填满的最大 swizzle({ref}`chap_data_layout`)。下面的 demo 直接展示这个约束:16 x 16 tile 上的 128-byte swizzle 只有在 tile 被切成匹配 atom 的 16 x 8 group 之后,才会变成 conflict-free。
```{raw} html
+
+ style="width:100%; min-width:1320px; height:640px; border:1px solid var(--pst-color-border, #d0d0d0); border-radius:6px;">
+
+
```
-
-*点击图中组件查看细节:TODO:翻译 tiling constraint 图注。*
+*交互图:16 x 16 tile 上的 128-byte swizzle;切成 16 x 8 group 后变成 conflict-free。*
## 完成通知:Load
-TODO:翻译 “Completion: Loads” 小节。
+Copy 是异步的,所以发起它还不够。Consumer 不能因为 TMA 指令已经发出就读取 shared-memory tile。只有在引擎写完 byte 之后,读取 tile 才是安全的。
+
+对于 TMA load,完成信号是 `mbarrier`({ref}`chap_async_barriers`)。
-
+常见顺序是:
+
+1. 为 pipeline stage 初始化或复用一个 `mbarrier`;
+2. 告诉 barrier 这次 TMA transfer 预计会写入多少 byte;
+3. 发起 TMA load;
+4. 让 TMA engine 在 byte 到达时更新 barrier;
+5. consumer 在读取 shared-memory tile 之前等待这个 barrier phase。
+
+Byte count 通过如下操作设置:
+
+```text
+mbarrier.arrive.expect_tx(bytes)
+```
+
+它做两件事。第一,记录期望 transfer size。第二,它也执行 issuing thread 在 barrier 上的 arrival。Barrier 不会因为这个调用发生了就完成。它仍然等待 TMA engine 报告期望 byte 已经到达。
+
+Transfer 进行时,引擎会对 barrier 执行 complete-tx 更新。只有两个条件都满足时,barrier phase 才会翻转:arrival count 已经满足,并且 pending byte count 到达 0。
+
+Consumer 随后等待这个 barrier。对期望 phase 的 wait 完成后,shared-memory tile 就准备好了。此时 MMA 路径可以安全读取它。
+
+
+
+这与其他异步 producer-consumer 交接使用的是同一个 barrier 模型。Producer 是 TMA engine。Consumer 是 MMA 路径,或者任何其他读取 shared-memory tile 的代码。Barrier 是它们之间的显式交接。
## 完成通知:Store
-TODO:翻译 “Completion: Stores” 小节。
+TMA store 沿相反方向移动数据,从 shared memory 到 global memory。它们也是异步的,但完成机制不同。
+
+TMA load 通常喂给同一个 kernel 内部的 consumer。MMA 路径需要知道 shared-memory tile 何时就绪。因此 load 路径使用 `mbarrier`。
+
+TMA store 通常把最终数据写出到 global memory。通常没有立即在 kernel 内等待存储结果的 consumer。Kernel 主要需要知道的是,什么时候可以复用 shared-memory buffer,或者结束 store 序列。
+
+为此,TMA store 使用 commit group 和 wait group。Kernel 发起一次或多次 store,commit 这个 group,随后等待这个 group drain。Wait 完成后,从 kernel 视角看,这个 group 中的 store 已经完成,store 使用的 shared-memory 区域可以安全复用。
+
+规则很简单:
+
+```text
+TMA load: wait through an mbarrier with byte-count tracking
+TMA store: wait through a commit group and wait group
+```
+
+这两个机制在不同交接点服务于同一个目的。Load 需要让 shared-memory tile 对后续 consumer 可见。Store 需要确保 outgoing transfer 在 kernel 复用 source storage 或依赖 store 已 drain 之前完成。
## 为什么 TMA 对 Pipelining 很重要
-TODO:翻译 “Why TMA Matters for Pipelining” 小节。
+TMA 最有用的场景是作为 pipeline 的一部分。Kernel 可以在 Tensor Core 计算当前 tile 时,发起未来 tile 的 load。Load 在后台运行。Compute 在前台运行。当未来 tile 变成当前 tile 时,barrier 把两者连接起来。
+
+典型 GEMM loop 会反复使用这个结构。Shared memory 的一个 stage 保存当前被 MMA 消费的 tile。另一个 stage 正在被 TMA 填充。Loop 前进时,这些角色轮换。在 MMA 读取某个 stage 之前,它会等待该 stage 的 load barrier。在 TMA 覆盖某个 stage 之前,kernel 会确保上一个 consumer 已经使用完它。
+
+这就是为什么 TMA 和 `mbarrier` 通常一起出现在 Blackwell 和 Hopper 风格的 kernel 中。TMA 给 kernel 一个异步 copy engine。Barrier 给 kernel 一个精确方式来知道 copy 出来的 byte 何时准备好。
diff --git a/zh/chapter_tmem/index.md b/zh/chapter_tmem/index.md
index 99746ba0..a66aea83 100644
--- a/zh/chapter_tmem/index.md
+++ b/zh/chapter_tmem/index.md
@@ -1,35 +1,76 @@
(chap_tmem)=
# 特殊内存:TMEM
-
-
:::{admonition} 概览
:class: overview
-- TODO:翻译本章 overview 第一条。
-- TODO:翻译本章 overview 第二条。
-- TODO:翻译本章 overview 第三条。
+- TMEM 是 Blackwell 上 `tcgen05` 使用的专用内存空间。它是每个 SM 上的二维暂存区,包含 128 个 Lane 行和最多 512 个 Col 列。
+- `tcgen05.mma` 会把 accumulator 写入 TMEM。Block-scaled MMA 也会用 TMEM 保存 scale factor。
+- TMEM 通过 Lane 和 Col 寻址。在 TIRx 的布局记号里,这两个硬件轴写作 `TLane` 和 `TCol`。
+- TMEM 不像寄存器那样自动分配。Kernel 必须以 32 列为单位显式分配和释放 TMEM。
+- 普通 shared-memory load/store 不能访问 TMEM。TMEM、寄存器和 shared memory 之间的数据搬运需要通过专用的异步 `tcgen05` 指令完成。
:::
-TODO:翻译导言部分。
+在 Hopper 以及更早的 GPU 上,Tensor Core({ref}`chap_tensor_cores`)的 accumulator 位于寄存器中。这个模型很容易理解:MMA 指令产生一个寄存器 fragment,kernel 在计算阶段让这个 fragment 保持存活,epilogue 随后读取它、做类型转换,并把结果存出去。
+
+问题在于寄存器压力。寄存器是固定的 per-thread 资源。随着 MMA tile 变大,accumulator fragment 也会变大。到一定程度之后,accumulator 会挤占线程需要保存的其他值。更大的 tile 有利于 Tensor Core 吞吐,但把整个 accumulator 都放在寄存器里,会让这些大 tile 更难使用。
+
+Blackwell 改变了这段数据路径。`tcgen05` 的 accumulator 不必在整个计算阶段都留在寄存器里。相反,`tcgen05.mma` 会把 accumulator 写入 Tensor Memory,也就是 TMEM。TMEM 是早期 NVIDIA GPU 没有的内存空间。它是 SM 上的一个二维暂存区,形状是 128 个 Lane 行乘以最多 512 个 Col 列,并且作用域属于使用它的 CTA。
+
+这个额外的内存空间让 Blackwell 可以支持更大的 Tensor Core tile,而不必把完整 accumulator 压到每个线程的寄存器里。但 TMEM 并不像寄存器那样自动存在。编译器不会把它当作普通寄存器存储直接分配给程序。Kernel 必须分配 TMEM,用正确的布局寻址,通过正确的指令搬入搬出,并在 CTA 完成后释放它。
## 二维地址空间
-TODO:翻译 “A 2D Address Space” 小节。
+TMEM 不是一个扁平的 byte array。它是二维地址空间。硬件把两个坐标称为 Lane 和 Col。TMEM 有 128 个 Lane 行,最多 512 个 Col 列。每个 Col 是一个 32-bit 列。
+
+这个形状很重要,因为 `tcgen05.mma` 会按照这个二维结构把 accumulator 写入 TMEM。一个 TMEM 位置由 Lane 坐标和 Col 坐标描述,而不是由一个类似 shared memory byte offset 的单一地址描述。
-
+当 kernel 在 TIRx 中声明 TMEM buffer 时,会给这个 buffer 一个覆盖这两个硬件坐标的布局。在布局记号({ref}`chap_data_layout`)里,我们把 TMEM Lane 轴写作 `TLane`,把 TMEM Col 轴写作 `TCol`。这些名字不是要替代官方硬件术语,而是 DSL 中的布局轴名,用来显式表达 TMEM 的两个维度。
+
+例如,一个 accumulator tile 可以写作:
+
+```text
+S[(128, N) : (1@TLane, 1@TCol)]
+```
+
+这表示 tile 沿硬件 Lane 维度有 128 行,沿硬件 Col 维度有 `N` 列。在布局记号中,这两个维度分别是 `TLane` 和 `TCol`。这个布局是直接映射:相邻行沿 `TLane` 移动,相邻列沿 `TCol` 移动。下图展示了这个网格,其中硬件 Lane 沿 128 行向下,硬件 Col 沿列方向展开。
+
+
+
+核心要点是:TMEM 是 tile 布局问题的一部分。它不是 Tensor Core 背后的隐藏存储。Kernel 必须命名这块内存,从中分配列,并使用与 `tcgen05` 指令读写方式匹配的布局。
## 分配
-TODO:翻译 “Allocation” 小节。
+Kernel 使用 TMEM 之前,必须先预留空间。这一点不同于寄存器。寄存器由编译器分配,而 TMEM 由 kernel 显式分配。
+
+分配按 CTA 进行。CTA 中的一个 warp 会请求一段 TMEM 列。请求以 32 列为单位,列数会按照硬件分配规则向上取整。分配完成后,CTA 会得到一个 base TMEM address。后续 `tcgen05` 指令用这个 base address 访问预留区域。
+
+把 TMEM 理解成一种有预算的 CTA 资源很有用,它类似 shared memory。CTA 拥有它分配到的 TMEM 列。Kernel 决定 accumulator、scale factor 或临时 staging 需要多少列。CTA 完成后,必须释放这段分配。
+
+因此,TMEM 也是 kernel 资源规划的一部分。更大的 accumulator tile 可能提高 Tensor Core 吞吐,但会消耗更多 TMEM 列。Block-scaled MMA 可能还需要额外 TMEM 空间来保存 scale factor。Kernel 必须让这些用途都落在可用的 TMEM 预算内,就像必须让 shared-memory buffer 落在 SMEM 预算内一样。
## 读写 TMEM
-TODO:翻译 “Reading and Writing TMEM” 小节。
+普通 `ld.shared` 和 `st.shared` 指令不能访问 TMEM。TMEM 是单独的地址空间,因此数据必须通过专用 `tcgen05` 指令移动。
+
+主要有三条路径。
+
+第一条路径是 `tcgen05.ld`,它把数据从 TMEM 加载到寄存器中。这是 MMA 阶段之后 epilogue 使用的路径。Accumulator 已经在 TMEM 中产生,但 epilogue 通常需要寄存器 fragment,才能做类型转换、执行 elementwise 操作并写出最终结果。
+
+在 DSL 层面,TMEM load 分布在一个 warpgroup 上。它会 lowering 成四个 warp-level 的 `tcgen05.ld` 操作,每个 warp 一个。每个 warp 处理 128 个 TMEM Lane 行中的 32 行,因此四个 warp 合起来覆盖完整的 Lane 维度。在布局记号里,这个完整维度就是 `TLane` 轴。
+
+指令本身来自一组 load shape,例如 `.16x64b`、`.16x128b`、`.16x256b`、`.32x32b` 和 `.16x32bx2`,并带有从 `.x1` 到 `.x128` 的 repeat factor。选择的 shape 决定读取多少个 TMEM 列,以及每个线程会收到多少个寄存器。
+
+重要结果是寄存器 fragment 的布局。对于常见的 epilogue 路径,lane `l` 会收到来自 TMEM 行 `l / 4` 以及两个列位置的值。这会产生与早期世代从 MMA 直接暴露出的 per-lane accumulator fragment 同类的布局({ref}`chap_layout_generations`)。这种连续性很重要:虽然 Blackwell 的 accumulator 在计算阶段位于 TMEM 中,但 epilogue 仍然可以复用 Ampere `mma` 或 Hopper `wgmma` 中已经使用过的寄存器级 cast 和 store 结构。
+
+
+
+第二条路径是 `tcgen05.st`,它把数据从寄存器写回 TMEM。这是 `tcgen05.ld` 的反方向。当线程已经持有一个寄存器 fragment,并且需要把它放入 TMEM 时会使用这条路径。例如,某些操作数或中间值可能会先经过寄存器 staging,再写入 TMEM 供后续 `tcgen05` 操作使用。
+
+第三条路径是 `tcgen05.cp`,它把数据从 shared memory 拷贝到 TMEM。这是一条 bulk copy 路径,常用于 block-scaled MMA 的 scale factor。在这种情况下,TMA 或普通线程代码会先把 scale 数据准备到 shared memory 中,然后 `tcgen05.cp` 把它移动到 Tensor Core 期望的 TMEM 布局里。
+
+这三条路径都是异步的。`tcgen05.ld`、`tcgen05.st` 或 `tcgen05.cp` 指令可能在数据搬运完成之前就返回。因此,kernel 在消费结果或复用存储之前,必须使用正确的完成机制({ref}`chap_async_barriers`)。
+
+等待路径取决于具体指令。`tcgen05.ld` 通过 `tcgen05.wait::ld` 完成。`tcgen05.st` 通过 `tcgen05.wait::st` 完成。`tcgen05.cp` 和 `tcgen05.mma` 一样,通过 commit group 和 `mbarrier` 完成。如果数据需要从一组线程交给另一组线程,kernel 还可能需要 fence,确保接收方线程按预期顺序看到已完成的写入。
-
+TMEM 位于 Blackwell Tensor Core 数据路径的中间。TMA 把操作数 staged 到 shared memory。`tcgen05.mma` 读取操作数并累加到 TMEM。对于 block-scaled MMA,scale factor 也可以 staged 到 TMEM。计算阶段结束后,`tcgen05.ld` 把 accumulator 带回寄存器,epilogue 再转换并存出最终输出。
diff --git a/zh/conf.py b/zh/conf.py
index 06ccfb07..70f80e5f 100644
--- a/zh/conf.py
+++ b/zh/conf.py
@@ -28,23 +28,6 @@
"**/README.md",
"_*.md",
"**/_*.md",
- # Release the Chinese edition chapter by chapter. Keep draft sources in
- # zh/, but exclude unreleased pages so they are not published or searchable.
- "appendix/**",
- "chapter_async_barriers/**",
- "chapter_clc/**",
- "chapter_data_layout/**",
- "chapter_flash_attention/**",
- "chapter_gemm_advanced/**",
- "chapter_gemm_async/**",
- "chapter_gemm_basics/**",
- "chapter_intro_tirx/**",
- "chapter_layout_generations/**",
- "chapter_tensor_cores/**",
- "chapter_tirx_layout_api/**",
- "chapter_tma/**",
- "chapter_tmem/**",
- "tirx_guide/**",
]
html_theme = "sphinx_book_theme"
diff --git a/zh/index.md b/zh/index.md
index 402b4224..9cd9b6ca 100644
--- a/zh/index.md
+++ b/zh/index.md
@@ -19,18 +19,51 @@
- **第四部分:Flash Attention 4。** 这一部分基于第三部分的技术构建完整的 attention kernel:两个 MMA,中间插入 softmax,并包含 online-softmax rescaling、causal masking 和 GQA。
- **附录。** TIRx API 和编译器内部机制说明。
-## 已发布章节
-
```{toctree}
:caption: 第一部分:理解 GPU
:maxdepth: 1
chapter_background/index
chapter_performance/index
+chapter_data_layout/index
+chapter_layout_generations/index
+chapter_tma/index
+chapter_tensor_cores/index
+chapter_tmem/index
+chapter_async_barriers/index
+chapter_clc/index
+```
+
+```{toctree}
+:caption: 第二部分:TIRx 概览
+:maxdepth: 1
+
+chapter_intro_tirx/index
+chapter_tirx_layout_api/index
+```
+
+```{toctree}
+:caption: 第三部分:GEMM:从 Tiled 到 SOTA
+:maxdepth: 2
+
+chapter_gemm_basics/index
+chapter_gemm_async/index
+chapter_gemm_advanced/index
+```
+
+```{toctree}
+:caption: 第四部分:Flash Attention 4
+:maxdepth: 2
+
+chapter_flash_attention/index
```
-
+```{toctree}
+:caption: 参考资料
+:maxdepth: 1
+
+appendix/index
+appendix/debugging_warp_specialized
+tirx_guide/arch/index
+tirx_guide/language_reference/index
+```
diff --git a/zh/tirx_guide/arch/index.md b/zh/tirx_guide/arch/index.md
index 9dcd21f3..faeee895 100644
--- a/zh/tirx_guide/arch/index.md
+++ b/zh/tirx_guide/arch/index.md
@@ -1,10 +1,10 @@
----
-orphan: true
----
-
(chap_arch)=
# 编译器内部机制
-> 翻译状态:待翻译。对应英文目录:`tirx_guide/arch/`。
+面向贡献者的 TIRx 编译器内部机制说明。
+
+```{toctree}
+:maxdepth: 1
-本页用于放置 TIRx 编译器内部机制的中文参考内容。
+lowering_pipeline
+```
diff --git a/zh/tirx_guide/arch/lowering_pipeline.rst b/zh/tirx_guide/arch/lowering_pipeline.rst
new file mode 100644
index 00000000..c0204673
--- /dev/null
+++ b/zh/tirx_guide/arch/lowering_pipeline.rst
@@ -0,0 +1,158 @@
+TIRx lowering pipeline
+======================
+
+``tvm.compile(mod, target, tir_pipeline="tirx")`` 会把作者写出的 TIRx module 送入 **tirx pipeline**。这是一组有序的 TIR pass,会把你写下的高层构造(tile primitive、带 ``TileLayout`` 的 buffer、execution-scope id)变成拆分后的 **host** + **device** function,然后由 CUDA backend 渲染成源码。Pipeline 定义在 ``python/tvm/tirx/compilation_pipeline.py``(``tirx_pipeline``)中;本页按顺序介绍这些 pass。
+
+它位于哪里
+----------
+
+``tvm.compile`` 会先绑定 target,运行 **tirx pipeline**(下面这些 module-level pass),然后分别对 host 和 device function 应用 **finalization** pass,最后把每个 device function 交给 CUDA code generator:
+
+.. code-block:: text
+
+ authored TIRx ──BindTarget──▶ tirx_pipeline ──▶ host func ──host finalize──▶ C/LLVM
+ │
+ └──────────▶ device func ──device finalize──▶ CUDA
+
+这些 Pass
+---------
+
+``tirx_pipeline`` module pass 会应用下面这个精确序列(少数 pass 受 ``PassContext`` config 控制):
+
+.. list-table::
+ :header-rows: 1
+ :widths: 6 32 62
+
+ * - #
+ - Pass
+ - 作用
+ * - 1
+ - ``LowerTIRx``
+ - 核心 lowering,见下方 `Inside LowerTIRx`_
+ * - 2
+ - ``UnifyThreadBinding``
+ - 合并等价的 thread-axis binding,使每个 ``threadIdx`` / ``blockIdx`` 轴只声明一次
+ * - 3
+ - ``StmtSimplify``
+ - statement-level 算术简化(arith analyzer)
+ * - 4
+ - ``LowerTIRxOpaque``
+ - 把剩余 opaque TIRx 构造 lower 成普通 TIR
+ * - 5
+ - ``FlattenBuffer``
+ - 把多维 ``BufferLoad`` / ``BufferStore`` 展平成 1-D
+ * - 6
+ - ``BF16ComputeLegalize``
+ - 把 ``bfloat16`` compute 重写成合法形式(f32 up-cast)
+ * - 7
+ - ``NarrowDataType(32)``
+ - 在可证明安全时把 index/loop ``PrimExpr`` dtype 缩窄到 32-bit
+ * - 8
+ - ``VectorizeLoop``
+ - 把 ``T.vectorized`` loop 变成 vector op(如果 ``tir.disable_vectorize`` 则跳过)
+ * - 9
+ - ``UnrollLoop``
+ - 展开标记为 ``T.unroll`` 的 loop(以及小的 constant loop)
+ * - 10
+ - ``StmtSimplify``
+ - 在 vectorize/unroll 暴露常量后再次简化
+ * - 11
+ - ``CommonSubexprElim``
+ - 把重复 subexpression hoist 到临时变量(如果 ``tir.disable_cse_tir`` 则跳过)
+ * - 12
+ - ``FP8ComputeLegalize``
+ - 把 ``float8`` compute 重写成合法形式
+ * - 13
+ - ``VerifyMemory``
+ - 检查 host-side code 不会直接 dereference device memory(安全门)
+ * - 14
+ - ``AnnotateEntryFunc``
+ - 把单个 PrimFunc 标记为 module entry point
+ * - 15
+ - ``SplitHostDevice``
+ - 在 ``launch_thread`` 边界把每个 kernel 拆成 **host** function 和 **device** function
+ * - 16
+ - ``MakePackedAPI``
+ - 把 host function 重写成 packed-func ABI(TVM 调用的 launcher)
+ * - 17
+ - ``FP8StorageLegalize``
+ - legalize ``float8`` storage(打包进支持的 container type)
+ * - 18
+ - ``BF16StorageLegalize``
+ - legalize ``bfloat16`` storage
+
+**Finalization** 随后按 function kind 分别运行:
+
+- **host**:``LowerTVMBuiltin`` (lower ``tvm_*`` builtin)、``LowerIntrin`` (target-specific intrinsic)
+- **device**:``LowerWarpMemory`` (warp-scoped buffer -> shuffle)、``StmtSimplify``、``LowerIntrin``
+
+Inside LowerTIRx
+----------------
+
+``LowerTIRx`` 本身是一个小序列(``src/tirx/transform/lower_tirx.cc``):
+
+.. code-block:: text
+
+ LowerTIRx = Sequential([ TilePrimitiveDispatch, LowerTIRxCleanup ])
+
+- **``TilePrimitiveDispatch``** 会把每个 ``TilePrimitiveCall``(``copy``、``gemm``、``reduction`` 等)替换成其选定 backend dispatch 生成的 body,也就是 variant selection 和 codegen。
+- **``LowerTIRxCleanup``** 会运行 ``LayoutApplier``:把每个带 ``TileLayout`` 的 buffer access 解析成具体物理地址算术(``addr = data + elem_offset + layout.apply(coord)``),展平 buffer,并把 execution-scope id(``T.cta_id`` / ``T.thread_id`` 等)lower 成 ``launch_thread`` 上的 ``blockIdx`` / ``threadIdx``。
+
+因此,``LowerTIRx`` 之后 module 已经是普通 TIR:没有 tile primitive,没有 ``TileLayout`` 间接层,scope id 已解析成 thread axis。
+
+一个 worked example
+-------------------
+
+看一个一行 scale kernel:
+
+.. code-block:: python
+
+ @T.prim_func
+ def scale(A_ptr: T.handle, B_ptr: T.handle):
+ A = T.match_buffer(A_ptr, (256,), "float32")
+ B = T.match_buffer(B_ptr, (256,), "float32")
+ T.device_entry(); bx = T.cta_id([1]); tx = T.thread_id([256])
+ B[tx] = A[tx] * T.float32(2.0)
+
+**``LowerTIRx`` 之后**,scope id 变成真实 thread axis,layout 已经应用(``A_1`` / ``B_1`` 是展平后的 1-D view):
+
+.. code-block:: python
+
+ with T.launch_thread("blockIdx.x", 1) as blockIdx_x:
+ threadIdx_x = T.launch_thread("threadIdx.x", 256)
+ bx: T.let = blockIdx_x
+ tx: T.let = threadIdx_x
+ B_1[threadIdx_x] = A_1[threadIdx_x] * T.float32(2.0)
+
+**``SplitHostDevice`` + ``MakePackedAPI`` 之后**,一个 function 变成两个:host launcher 和 device kernel:
+
+.. code-block:: python
+
+ @I.ir_module
+ class Module:
+ def main(...): # host: packed-API launcher (computes the grid/block, launches)
+ ...
+ def scale_kernel(...): # device: the __global__ body, run on the GPU
+
+CUDA backend 随后把 ``scale_kernel`` 渲染成 ``__global__`` function(``B_ptr[threadIdx.x] = A_ptr[threadIdx.x] * 2.0f``)。
+
+自己复现
+--------
+
+你可以手动运行 pipeline 的任意前缀来检查某个阶段。这些文档中的 IR snippet 就是这样生成的:
+
+.. code-block:: python
+
+ from tvm.tirx import transform as TT
+
+ target = tvm.target.Target("cuda")
+ mod = TT.BindTarget(target.with_host("llvm"))(tvm.IRModule({"main": scale}))
+ mod = TT.LowerTIRx()(mod) # tile primitives dispatched, layouts applied
+ print(mod.script()) # inspect the lowered TIRx IR
+
+也可以编译整个 module 并读取生成的 CUDA:
+
+.. code-block:: python
+
+ exe = tvm.compile(tvm.IRModule({"main": scale}), target=target, tir_pipeline="tirx")
+ print(exe.mod.imports[0].inspect_source())
diff --git a/zh/tirx_guide/language_reference/cuda/buffers.rst b/zh/tirx_guide/language_reference/cuda/buffers.rst
new file mode 100644
index 00000000..27377b92
--- /dev/null
+++ b/zh/tirx_guide/language_reference/cuda/buffers.rst
@@ -0,0 +1,366 @@
+Buffer 与 memory
+================
+
+参数 buffer 使用 ``T.match_buffer`` 绑定;临时 scratch buffer 在函数体中使用下面两类声明 API 创建。用 ``A[i, j]`` 索引 buffer,用 ``A[m0:m0+BM, 0:BK]`` 切片(得到 ``BufferRegion``),并用 ``A.ptr_to([i, j])`` 取得 pointer,或用 ``A.data`` 取得原始 data pointer。
+
+声明 buffer
+-----------
+
+有两个基础 API 会创建 buffer:
+
+- ``T.alloc_buffer(shape, dtype, scope=..., ...)`` — 分配新的 storage(生成 ``AllocBuffer`` 节点),并返回 ``Buffer``。``T.alloc_shared`` / ``T.alloc_local`` 只是带有 ``scope="shared"`` / ``scope="local"`` 的 ``alloc_buffer``。
+- ``T.decl_buffer(shape, dtype, data=..., ...)`` — 在已有 pointer ``data`` 上声明一个 view(不分配);用于给 storage 起别名或重新解释 storage,例如 pool 的某个子区域,或一个 tensor-memory address。当 ``data=None`` 时,它会像 ``alloc_buffer`` 一样分配。
+
+Buffer 的 ``data`` pointer 是一个 immutable ``Var``(``alloc_buffer`` 会定义它;``decl_buffer`` 会接收它)。如果要用一个 pointer *expression* 作为 buffer backing,需要先把它绑定起来,参见 :doc:`data_types`。
+
+二者共用同一类 descriptor;最重要的参数如下:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 28 72
+
+ * - 参数
+ - 含义
+ * - ``dtype``
+ - element type,例如 ``"float32"``、``"float16"``、``"float4_e2m1fn"`` 等
+ * - ``shape``
+ - 逻辑 shape(一组 extent)
+ * - ``layout``
+ - 物理映射(:ref:`TileLayout `);``"default"`` 表示 dense row-major
+ * - ``elem_offset`` / ``allocated_addr``
+ - ``elem_offset``(或 ``byte_offset``)把一个 *view* 放到 ``data`` 中的某个 offset;``allocated_addr`` 携带预先分配的 address(tensor memory)
+ * - ``align``
+ - data pointer 的对齐,以 byte 为单位
+
+``scope`` 参数选择 memory space:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 26 22 52
+
+ * - Scope
+ - 简写
+ - Memory
+ * - ``"global"``
+ - (默认)
+ - device global memory
+ * - ``"shared"``
+ - ``T.alloc_shared``
+ - static shared memory(``__shared__``)
+ * - ``"shared.dyn"``
+ - (pool)
+ - dynamic shared memory(pooled,见下文)
+ * - ``"local"``
+ - ``T.alloc_local``
+ - per-thread register
+ * - ``"tmem"``
+ - (TMEM pool)
+ - Blackwell tensor memory(见下文)
+
+.. code-block:: python
+
+ A = T.match_buffer(A_ptr, (M, K), "float16", align=16) # parameter buffer
+ As = T.alloc_shared((BM, BK), "float16") # new shared tile
+ acc = T.alloc_local((4,), "float32") # register accumulator
+ view = T.decl_buffer((BM, BK), "float16", data=As.data) # a view over As
+
+**基于 pointer 的 buffer 只是 pointer 之上的 metadata。** 对任何非 tmem buffer 而言,声明就是一个 pointer 加一个 layout,索引会解析成 address::
+
+ addr(buffer[coord]) = buffer.data + elem_offset + layout.apply(coord, shape=shape)["m"]
+
+(``layout.apply`` 返回逐轴映射;其中 ``"m"`` 分量是 element offset。)因此,同一个逻辑访问会完全根据 buffer metadata 编译成不同的 address arithmetic。在 4×8 区域上写 ``B[i, j] = A[i, j] + 1``,如果用四种方式声明 ``B``:
+
+.. code-block:: python
+
+ from tvm.tirx.layout import TileLayout, S
+
+ B = T.match_buffer(p, (4, 8), "float32") # row-major
+ B = T.match_buffer(p, (4, 8), "float32", layout=TileLayout(S[(4, 8):(1, 4)])) # column-major
+ B = T.match_buffer(p, (4, 8), "float32", elem_offset=64) # shifted view
+ B = T.match_buffer(p, (4, 8), "float32", layout=TileLayout(S[(4, 8):(16, 1)])) # row stride 16
+
+那么每一种都会让 ``B[i, j]`` lower 成生成 CUDA 中不同的 index(``A[i, j]`` load 仍然是 ``i*8 + j``,只有 ``B`` 的 metadata 发生了变化):
+
+.. code-block:: c++
+
+ B_ptr[((i * 8) + j)] = ...; // row-major: i*8 + j
+ B_ptr[((j * 4) + i)] = ...; // column-major: j*4 + i
+ B_ptr[(((i * 8) + j) + 64)] = ...; // elem_offset=64: i*8 + j + 64
+ B_ptr[((i * 16) + j)] = ...; // row stride 16: i*16 + j
+
+Shared memory
+-------------
+
+Shared memory 有两种形式:static(编译时固定)和 dynamic(launch 时指定大小),此外还有一个 pool helper 用于管理 dynamic 情况。
+
+Static
+~~~~~~
+
+最简单的 shared buffer 是 **static** buffer,也就是 ``T.alloc_shared``(即 ``scope="shared"``),其大小在编译时确定。把数据 stage 到其中,执行 ``cta_sync`` 让整个 block 看到写入,然后再读回:
+
+.. code-block:: python
+
+ @T.prim_func
+ def smem_demo(A_ptr: T.handle, B_ptr: T.handle):
+ A = T.match_buffer(A_ptr, (128,), "float32")
+ B = T.match_buffer(B_ptr, (128,), "float32")
+ T.device_entry()
+ bx = T.cta_id([1])
+ tx = T.thread_id([128])
+ sm = T.alloc_shared((128,), "float32") # static shared memory
+ sm[tx] = A[tx]
+ T.cuda.cta_sync()
+ B[tx] = sm[tx] * T.float32(2.0)
+
+它会 lower 成普通的 ``__shared__`` array(生成 CUDA,省略 boilerplate):
+
+.. code-block:: c++
+
+ extern "C" __global__ void __launch_bounds__(128)
+ smem_demo_kernel(float* __restrict__ A_ptr, float* __restrict__ B_ptr) {
+ int tx = ((int)threadIdx.x);
+ __shared__ alignas(64) float sm_ptr[128]; // T.alloc_shared
+ sm_ptr[tx] = A_ptr[tx];
+ __syncthreads(); // T.cuda.cta_sync()
+ B_ptr[tx] = sm_ptr[tx] * 2.0f;
+ }
+
+Dynamic
+~~~~~~~
+
+**Dynamic** shared memory(``scope="shared.dyn"``)的大小按 launch 设置(``sharedMemBytes`` launch parameter),而不是在编译时固定。一个 kernel 只能有 **一个** dynamic-shared allocation,也就是 *arena*。因此需要分配一次 arena,再用 ``T.decl_buffer`` 和 arena pointer 加 ``elem_offset`` 把每个 buffer 声明成其中的一个 view:
+
+.. code-block:: python
+
+ arena = T.alloc_buffer((128,), "float32", scope="shared.dyn") # the one arena
+ As = T.decl_buffer((64,), "float32", data=arena.data, scope="shared.dyn") # offset 0
+ Bs = T.decl_buffer((64,), "float32", data=arena.data, elem_offset=64, scope="shared.dyn") # offset 64
+ As[tx] = A[tx]
+ Bs[tx] = B[tx]
+ T.cuda.cta_sync()
+ C[tx] = As[tx] + Bs[tx]
+
+两个 view 共享同一个 ``extern __shared__`` arena(生成 CUDA,省略 boilerplate;这里为清晰起见把 arena 命名为 ``smem``):
+
+.. code-block:: c++
+
+ extern __shared__ __align__(64) float smem[]; // the one dynamic-shared arena
+ smem[tx] = A_ptr[tx]; // As — view at offset 0
+ smem[tx + 64] = B_ptr[tx]; // Bs — view at offset 64
+ __syncthreads();
+ C_ptr[tx] = smem[tx] + smem[tx + 64];
+
+(两个独立的 ``alloc_buffer(scope="shared.dyn")`` 调用是错误的,**只允许一次 dynamic shared memory allocation**。)所以,static shared memory 的大小在编译时确定(``__shared__ T x[N];``);dynamic shared memory 则是这个按 launch 指定大小的唯一 arena,其他 buffer 是在其中以 offset 声明出来的 view。
+
+.. note::
+
+ **TVM 如何标注 dynamic-shared 大小。** Arena 的大小在编译时已知(这里是 ``128`` 个 float,即 ``512`` byte)。lowering 时,TVM 会向 device kernel 的 ``tirx.kernel_launch_params`` 追加一个 ``"tirx.use_dyn_shared_memory"`` tag,host launcher 会计算总 byte 数,并作为最后一个 launch argument 传入:
+
+ .. code-block:: python
+
+ # device kernel attribute:
+ "tirx.kernel_launch_params": ["blockIdx.x", "threadIdx.x", "tirx.use_dyn_shared_memory"]
+
+ # host-side launch call (..., gridDim.x, blockDim.x, dyn_shared_bytes):
+ T.call_packed("dyn_kernel", A.data, B.data, C.data, 1, 64, 512)
+
+ 运行时这个 ``512`` 会变成 ``cuLaunchKernelEx`` 调用中的 ``config.sharedMemBytes``。你不需要手动设置它;它由 ``shared.dyn`` allocation 的大小推导出来。
+
+Pool sugar
+~~~~~~~~~~
+
+``T.SMEMPool`` 会自动处理 arena bookkeeping:它以 bump allocation 的方式分配 offset,因此不需要手写 ``decl`` view。除了 ``alloc`` / ``commit`` 之外,它还提供每个 buffer 的 ``align=``、``alloc_mma`` helper(自动构造 MMA 兼容的 swizzle layout),以及 ``move_base_to``,用于回退 cursor 并复用空间:
+
+.. code-block:: python
+
+ pool = T.SMEMPool() # bump allocator over shared.dyn
+ As = pool.alloc((BM, BK), "float16", align=128) # carve a tile
+ Bs = pool.alloc((BK, BN), "float16", align=128)
+ Cs = pool.alloc_mma((BM, BN), "float16") # MMA-compatible, swizzle inferred
+ pool.commit() # finalize the pool's size
+ # pool.move_base_to(offset) rewinds the cursor to reuse space
+
+TMEM pool(见下文 `Tensor memory`_)建立在 ``SMEMPool`` 之上。
+
+Registers
+---------
+
+Per-thread scratch 位于 register 中。用 ``T.alloc_local(shape, dtype)``(即 ``scope="local"``)分配它:它对每个 thread 私有,并 lower 成保存在 register 中的 local array。
+
+.. code-block:: python
+
+ r = T.alloc_local((4,), "float32") # per-thread register array
+ for k in T.unroll(4):
+ r[k] = A[tx, k]
+ # ... compute on r[0..3] ...
+
+.. code-block:: c++
+
+ alignas(64) float r_ptr[4]; // per-thread, register-resident
+ r_ptr[0] = A_ptr[tx * 4 + 0];
+ r_ptr[1] = A_ptr[tx * 4 + 1];
+ // ...
+
+.. note::
+
+ ``alignas(64)`` 是 *默认* buffer alignment:buffer 的 ``data_alignment`` 默认是 ``runtime::kAllocAlignment``(64 byte),CUDA codegen 会把它标到每个 allocation 上,包括 per-thread ``local`` array,即使这里没有实际意义。对这些 register-resident array 来说,它 **没有性能影响**:带有静态可解析 index 的 thread-local array 会被 nvcc/ptxas 提升到 register 中(scalar replacement of aggregates, SROA),因此它永远不会存在于可寻址的 local memory 中,alignment 也就是 no-op。(如果动态索引 array spill 到 local memory,它确实会带上这个过度对齐,但这不是常见情况。)register local 的这种过度对齐是一个已知粗糙点,我们计划修复(对 ``local`` scope 使用 dtype 的自然对齐)。
+
+Scalar
+~~~~~~
+
+Scalar 只是只有 **一个 element** 的 register array;严格来说,不需要单独的概念。你可以分配一个大小为 1 的 ``local`` buffer 并索引 ``[0]``:
+
+.. code-block:: python
+
+ phase = T.alloc_local((1,), "int32") # 1-element register array
+ phase[0] = 0
+ while phase[0] < 4:
+ acc = acc + A[tx, phase[0]]
+ phase[0] += 1
+
+但到处写 ``phase[0]`` 很笨重,所以 **scalar** 正是这件事的语法糖:一个单元素 register buffer,可以 **按名字** 读写:
+
+.. code-block:: python
+
+ phase: T.int32 = 0 # mutable scalar (sugar for the above)
+ while phase < 4:
+ acc = acc + A[tx, phase]
+ phase += 1
+
+ s = T.local_scalar("int32") # explicit form; assign by name (s = ..., not s[0])
+ acc: T.float32 = 0.0 # a type-annotated assignment also makes one
+
+二者不只是相似,而是会 parse 成 **结构完全相同的 TIRx**。这个语法糖完全在 parser 中消解:``phase: T.int32`` *就是* 那个单元素 ``local`` buffer,``phase`` / ``phase += 1`` *就是* ``phase[0]`` / ``phase[0] += 1``。对两个 kernel 调用 ``tvm.ir.assert_structural_equal`` 会通过,printer 甚至会把显式的 ``alloc_local`` + ``[0]`` 形式 **重新打印** 成 scalar 形式。因此,一旦 parsing 完成,二者完全没有区别。二者都会 lower 成同一个 ``alignas(64) int phase_ptr[1];``;scalar 只是让你省掉 ``[0]``。(``T.local_scalar`` / ``T.shared_scalar`` / ``T.alloc_scalar`` 会显式选择 scope。)
+
+.. note::
+
+ 为什么不用 ``Var`` ?TIRx ``Var`` 是 *immutable* 的,也就是一次性的静态绑定(正是下面 ``T.let`` 产生的东西)。Scalar 需要是 *mutable* 的:你会在 loop 和 accumulator 中重新赋值。因此它必须由可以反复 store 的单元素 buffer backing,而不是由 ``Var`` backing。
+
+``let``
+~~~~~~~
+
+``T.let`` binding 是 **immutable** 的,也就是一个 ``LetStmt`` (一个具名值,不是 buffer)。用它表示派生常量:
+
+.. code-block:: python
+
+ n: T.let = M * K # immutable binding (LetStmt)
+ half: T.let[T.int32] = N // 2 # ... with an explicit type
+
+它会 lower 成 **普通的 scalar C variable**,而不是 buffer(没有 array,也没有 ``[0]``)。例如 ``half: T.let = m * 2``(其中 ``m`` 是 runtime 值):
+
+.. code-block:: c++
+
+ int half = m * 2; // the `let` -> a const-like local
+
+由于值是 immutable 的,simplifier 可以自由传播它并对它做 CSE,所以在使用处你经常会看到 ``m * 2`` 被直接替换进去(或通过 common-subexpression 临时变量共享),而不是看到对 ``half`` 的引用。
+
+.. note::
+
+ **为什么需要 immutable binding?** 因为值不能改变,arithmetic analyzer 会把 var 绑定到该值(它简化 ``LetStmt`` 时会执行 ``analyzer.Bind(var, value)``),所以关于该值证明出来的事实,包括常量边界、modular set(divisibility / alignment)和 range,都会 **传播到每一次使用**。这会帮助 index simplification、bounds-check elimination,以及 alignment/vectorization 决策。*Mutable* scalar 是一次 memory load(``buf[0]``):analyzer 不能假设它保持不变,因此这些属性都无法传递。``let`` 也是一个纯 value:没有 allocation,并且可以自由 inline、substitute 或 CSE;而 scalar 是带有 load/store 语义的单元素 buffer。
+
+Tensor memory
+-------------
+
+Blackwell *tensor memory* 不是普通的 scratch scope:它必须通过 warp-uniform 的 ``T.ptx.tcgen05.alloc`` / ``tcgen05.dealloc`` intrinsic 显式 reserve 和 free;每个 tensor 都是其中的一个 view,通过 ``T.decl_buffer(..., scope="tmem", allocated_addr=, layout=)`` 声明。``allocated_addr``(column offset)是必需的,tensor-core dispatch 会断言它存在,因此 ``T.alloc_buffer(scope="tmem")``(它 **不会** 设置该字段)不能工作。与 shared memory 不同,tensor memory 不能被直接寻址:只能通过 ``tcgen05`` ``mma`` / ``ld`` / ``st`` / ``cp`` 读写。
+
+手写时,一个 warp 把 allocation 发到 shared slot 中,你用 column offset 把每个 tensor ``decl`` 成一个 view,最后由一个 warp free 它:
+
+.. code-block:: python
+
+ addr = T.alloc_shared((1,), "uint32") # slot for the allocated base
+ if warp_id == alloc_warp: # tcgen05.alloc is warp-uniform
+ T.ptx.tcgen05.alloc(T.address_of(addr), n_cols=512, cta_group=cta_group)
+ acc = T.decl_buffer((CTA_M, 512), "float32", scope="tmem",
+ allocated_addr=0, layout=tmem_layout) # view at column 0
+ # ... use acc as a gemm_async / copy_async operand ...
+ if warp_id == alloc_warp:
+ T.ptx.tcgen05.relinquish_alloc_permit(cta_group=cta_group)
+ T.ptx.tcgen05.dealloc(addr, n_cols=512, cta_group=cta_group)
+
+你需要自己管理 column offset 和 ``tmem_layout`` (一个 datapath D/F layout)。这正是下面 pool 会生成的序列。
+
+Pool
+~~~~
+
+``T.TMEMPool`` 会封装上述所有工作:warp-uniform alloc/dealloc、column bump-allocation,以及 datapath layout:
+
+.. code-block:: python
+
+ tmem_addr = pool.alloc((1,), "uint32") # pool = the kernel's smem pool
+ tmem_pool = T.TMEMPool(pool, total_cols=512, cta_group=cta_group,
+ tmem_addr=tmem_addr)
+ acc = tmem_pool.alloc((CTA_M, 512), "float32") # allocated_addr set for you
+ tmem_pool.commit() # emits tcgen05.alloc (one warp)
+ # ... use acc ...
+ tmem_pool.dealloc() # emits tcgen05.dealloc (one warp)
+
+完整示例见第三部分的 GEMM kernel。
+
+Buffer API
+----------
+
+``Buffer`` 是 pointer 之上的 metadata(见上文 *声明 buffer*),因此它的大部分方法都是 *compile-time* reshape/reinterpret:它们改变 index arithmetic,或给你一个 pointer,本身不会生成运行时操作。常用方法如下:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 34 66
+
+ * - 方法
+ - 含义
+ * - ``B.data``
+ - 原始 data pointer(一个 ``Var``);打印为 ``B_ptr``
+ * - ``B.ptr_to([i, j])``
+ - 指向某个 element 的 typed pointer(``address_of``);打印为 ``&B_ptr[…]``
+ * - ``B.vload([i], dtype="float32x4")`` / ``B.vstore([i], v)``
+ - vectorized load / store;打印为 ``*(float4*)(B_ptr + …)``
+ * - ``B.view(*shape, layout=…)``
+ - 在新的 shape/layout 下重新解释同一块 storage(无 copy)
+ * - ``B.local(*shape, layout=…)``
+ - ``local`` buffer 中属于调用 thread 的私有 register slice
+ * - ``B.permute(*dims)``
+ - 轴被置换后的 view(transposed layout)
+ * - ``B.access_ptr(mask, …)``
+ - masked access pointer(``tvm_access_ptr`` builtin),用于把一个 region 传给 intrinsic
+
+**Pointer:``ptr_to`` / ``data``。** ``ptr_to`` 用于把 element address 传给 intrinsic 或 inline function;``data`` 是 base pointer:
+
+.. code-block:: python
+
+ B[tx] = T.cuda.func_call("ld", A.ptr_to([tx]), source_code=SRC, return_type="float32")
+
+.. code-block:: c++
+
+ B_ptr[tx] = ld(&A_ptr[tx]); // ptr_to([tx]) -> &A_ptr[tx]; A.data -> A_ptr
+
+**Vectorized access:``vload`` / ``vstore``。** 用一次 wide transfer 移动多个 element(另见 :doc:`data_types`):
+
+.. code-block:: python
+
+ B.vstore([tx * 4], A.vload([tx * 4], dtype="float32x4"))
+
+.. code-block:: c++
+
+ *(float4*)(B_ptr + tx * 4) = *(float4*)(A_ptr + tx * 4);
+
+**Reshape / reinterpret:``view`` / ``permute``。** 二者都是纯 metadata;data pointer 不变,只是 index arithmetic 不同。``A.view(64, 4)`` 会把 256-element buffer 看作 ``64×4``;``A.permute(1, 0)`` 会转置轴:
+
+.. code-block:: python
+
+ A2 = A.view(64, 4); y = A2[tx, 0] + A2[tx, 3] # A2[tx, j] -> A_ptr[tx*4 + j]
+ At = A.permute(1, 0); z = At[i, j] # At[i, j] -> A_ptr[j*4 + i]
+
+.. code-block:: c++
+
+ A2_ptr[tx * 4] /* +3 */ // view: row-major 64x4 index
+ At_ptr[(j * 4) + i] // permute: swapped strides
+
+**Register:``local``。** 将 thread-axis ``local`` layout 分解成调用 thread 自己的扁平 register bundle(tile primitive 中大量使用):
+
+.. code-block:: python
+
+ R = T.alloc_buffer((32, 8), "float32", scope="local", layout=TileLayout(S[(32, 8) : (1 @ laneid, 1)]))
+ Rl = R.local(8) # this lane's 8 registers
+
+.. code-block:: c++
+
+ alignas(64) float Rl_ptr[8]; // the lane's private registers
diff --git a/zh/tirx_guide/language_reference/cuda/control_flow.rst b/zh/tirx_guide/language_reference/cuda/control_flow.rst
new file mode 100644
index 00000000..848c832f
--- /dev/null
+++ b/zh/tirx_guide/language_reference/cuda/control_flow.rst
@@ -0,0 +1,91 @@
+Control flow
+============
+
+Control flow 包括 ``if``、loop family 和 ``while``;它们都会映射到直观的 CUDA。
+
+if
+--
+
+Python ``if`` / ``else`` 会变成 CUDA ``if`` / ``else``。可以用 thread/lane 比较来 guard work,或者用 ``T.ptx.elect_sync()`` 选出一个 issuing thread:
+
+.. code-block:: python
+
+ if tx < 128:
+ A[tx] = A[tx] * T.float32(2.0)
+ else:
+ A[tx] = A[tx] + T.float32(1.0)
+
+ if T.ptx.elect_sync():
+ ... # one elected lane (e.g. to issue TMA/MMA)
+
+.. code-block:: c++
+
+ if (((int)threadIdx.x) < 128) {
+ A_ptr[tx] = A_ptr[tx] * 2.0f;
+ } else {
+ A_ptr[tx] = A_ptr[tx] + 1.0f;
+ }
+
+如果需要 expression-level choice(没有 branch),使用 ``T.if_then_else(cond, a, b)``。它会 lower 成 ternary,因此不引入 control-flow divergence:
+
+.. code-block:: c++
+
+ O_ptr[tx] = (A_ptr[tx] > 0.0f) ? A_ptr[tx] : 0.0f;
+
+Uniform vs. divergent control flow
+----------------------------------
+
+``if tx < 128`` 这类 per-thread guard 对普通 work 没问题,但 **collective** operation 必须被它同步的所有线程 *uniformly* 到达。
+
+例如,``T.cuda.cta_sync()`` 映射到 ``__syncthreads()`` ,需要 thread block 中所有线程到达。它绝不能放在 thread-divergent 或 warpgroup-divergent branch 内:如果放在 ``if wg_id == 0:`` 内,其他 warpgroup 永远不会到达,kernel 会 deadlock。当只有一个 warpgroup 需要同步时,使用 warpgroup-scoped ``T.cuda.warpgroup_sync(id)`` (见 :ref:`chap_gemm_advanced` 和 :doc:`threads_sync`)。
+
+同样的注意事项适用于 barrier setup。``mbarrier`` 的 ``.init()`` 会 lower 成 single-thread guard(``if (threadIdx.x < 1)``)。如果把它嵌到另一个 divergent branch 中,barrier 可能保持未初始化,导致 unspecified launch failure。
+
+loop
+----
+
+Loop 有四种形式;普通 Python ``range`` 会变成 ``T.serial``:
+
+- ``T.serial(n)``:顺序 loop(ptxas 仍可能 unroll 它)。
+- ``T.unroll(n)``:完全 unrolled(展开成 straight-line statement)。
+- ``T.vectorized(n)``:vectorized loop。
+- ``T.grid(*extents)``:嵌套 loop nest。
+
+``break`` / ``continue`` 可以在 loop 内使用。
+
+.. code-block:: python
+
+ for i, j in T.grid(8, 8):
+ B[i, j] = T.max(A[i, j], T.float32(0.0))
+
+.. code-block:: c++
+
+ for (int i = 0; i < 8; ++i)
+ for (int j = 0; j < 8; ++j)
+ B_ptr[i * 8 + j] = max(A_ptr[i * 8 + j], 0.0f);
+
+``T.unroll(4)`` 则展开成四条 straight-line statement,没有 loop。
+
+while
+-----
+
+``while`` loop 会运行到 condition 为 false。请使用 mutable scalar counter(见 :doc:`buffers`):
+
+.. code-block:: python
+
+ i: T.int32 = 0
+ while i < 64:
+ A[i] = A[i] + T.float32(1.0)
+ i += 1
+
+它会 lower 成带 early-exit ``break`` 的 ``while (1)`` (counter 是一个 one-element register buffer):
+
+.. code-block:: c++
+
+ int i_ptr[1];
+ i_ptr[0] = 0;
+ while (1) {
+ if (!(i_ptr[0] < 64)) { break; }
+ A_ptr[i_ptr[0]] = A_ptr[i_ptr[0]] + 1.0f;
+ i_ptr[0] = i_ptr[0] + 1;
+ }
diff --git a/zh/tirx_guide/language_reference/cuda/data_types.rst b/zh/tirx_guide/language_reference/cuda/data_types.rst
new file mode 100644
index 00000000..10f59951
--- /dev/null
+++ b/zh/tirx_guide/language_reference/cuda/data_types.rst
@@ -0,0 +1,84 @@
+Data types and expressions
+==========================
+
+每个 TIRx expression 都带有一个低层 **dtype** 和一个高层 **type**。
+
+Expression dtype
+----------------
+
+``PrimExpr`` 的 ``.dtype`` 是其 scalar(或 vector)element type,例如 ``float32``、``float16``、``bfloat16``、``int32``、``uint8``、``bool``、低精度 ``float8_e4m3fn`` / ``float4_e2m1fn``、``handle``(pointer),以及 ``float32x4`` 这样的 vector form。每种 dtype 都会打印成匹配的 CUDA type。下面示例跨多个 dtype 分配 local 和 shared buffer,并执行 vectorized ``float32x4`` load/store:
+
+.. code-block:: python
+
+ @T.prim_func
+ def dtypes(A_ptr: T.handle, O_ptr: T.handle):
+ A = T.match_buffer(A_ptr, (256,), "float32")
+ O = T.match_buffer(O_ptr, (256,), "float32")
+ T.device_entry(); bx = T.cta_id([1]); tx = T.thread_id([64])
+ f16 = T.alloc_local((1,), "float16") # register scalars ...
+ bf16 = T.alloc_local((1,), "bfloat16")
+ i32 = T.alloc_local((1,), "int32")
+ u8 = T.alloc_local((1,), "uint8")
+ b1 = T.alloc_local((1,), "bool")
+ sm = T.alloc_shared((64,), "float16") # ... and a shared tile
+ v = T.alloc_local((1,), "float32x4") # a vector-dtype register (float4)
+ v[0] = A.vload([tx * 4], dtype="float32x4") # vectorized load
+ O.vstore([tx * 4], v[0]) # vectorized store
+ # ... (use f16/bf16/i32/u8/b1/sm) ...
+
+它会 lower 成(生成 CUDA,省略部分):
+
+.. code-block:: c++
+
+ half f16_ptr[1]; // float16
+ nv_bfloat16 bf16_ptr[1]; // bfloat16
+ int i32_ptr[1]; // int32
+ uchar u8_ptr[1]; // uint8
+ signed char b1_ptr[1]; // bool
+ __shared__ alignas(64) half sm_ptr[64]; // shared float16
+ float4 v_ptr[1]; // float32x4 (vector)
+ v_ptr[0] = *(float4*)(A_ptr + tx * 4); // vectorized load
+ *(float4*)(O_ptr + tx * 4) = v_ptr[0]; // vectorized store
+
+Buffer 的 dtype 本身也可以是 **vector type**:``T.alloc_local((1,), "float32x4")`` 会直接声明一个 ``float4`` register(用 ``v[0]`` 索引),随后 ``float32x4`` 的 ``vload`` / ``vstore`` 会以一次 16-byte access 移动它。Vector dtype 不绑定到 ``vload``;任何 buffer 或 scalar 都可以携带它。
+
+dtype -> CUDA 映射如下:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 34 33 33
+
+ * - dtype → CUDA
+ - dtype → CUDA
+ - dtype → CUDA
+ * - ``float32`` → ``float``
+ - ``float16`` → ``half``
+ - ``bfloat16`` → ``nv_bfloat16``
+ * - ``int32`` → ``int``
+ - ``uint8`` → ``uchar``
+ - ``bool`` → ``signed char``
+ * - ``float32x4`` → ``float4``
+ - ``handle`` → ``T*`` (pointer)
+ - (vector dtypes → CUDA vector types)
+
+dtype vs type
+-------------
+
+``dtype`` 是 *low-level* 的,说明“哪些 bit”。另外,值还有一个高层 **type**:scalar 使用 ``PrimType(dtype)``,pointer 使用 ``PointerType(PrimType(dtype), scope)``。大多数 expression 是 scalar(``PrimType``);type system 主要在 **pointer** 上重要。
+
+Pointer(``handle``)
+--------------------------
+
+Buffer 的 ``data``,也就是 pointer,是 pointer type 的 ``Var``,并且是 immutable(pointer 不会被重新赋值)。这决定了你如何取得它:
+
+- ``T.alloc_buffer(...)`` 分配 storage,并定义它的 ``data`` pointer。
+- ``T.decl_buffer(..., data=ptr)`` 在已有 pointer ``Var`` ``ptr`` 上声明 buffer。
+- 如果要让 buffer 以 pointer **expression** 为 backing,例如 ``T.ptx.map_shared_rank``(PTX ``mapa``)给出的另一个 cluster CTA 的 shared address,必须先把这个 expression 绑定成 pointer ``Var``(``data`` 必须是 ``Var``,不能是 expression),使用 ``PointerType`` 的 ``T.let``:
+
+ .. code-block:: python
+
+ from tvm.ir.type import PointerType, PrimType
+
+ ptr: T.let[T.Var(name="ptr", dtype=PointerType(PrimType("uint64")))] = \
+ T.reinterpret("handle", T.ptx.map_shared_rank(mbar.ptr_to([0]), 0))
+ remote_mbar = T.decl_buffer([1], "uint64", data=ptr, scope="shared")
diff --git a/zh/tirx_guide/language_reference/cuda/parser_utils.rst b/zh/tirx_guide/language_reference/cuda/parser_utils.rst
new file mode 100644
index 00000000..27aac421
--- /dev/null
+++ b/zh/tirx_guide/language_reference/cuda/parser_utils.rst
@@ -0,0 +1,52 @@
+Parser utilities
+================
+
+少数 helper 会在 **parse time** 起作用,也就是 TVMScript 被转换成 TIRx 的阶段。它们允许你 inline Python 计算出来的值,抽出可复用片段,并打包 parser-side state。
+
+``T.meta_var`` — inline Python 值
+---------------------------------
+
+``T.meta_var(x)`` 告诉 parser 把 ``x``,一个在 **Python** 中计算出来的值,当作 compile-time *meta* value,并直接 inline 到 IR 中,而不是把它解析成 script variable。它可以避免一次性的 local,也驱动 metaprogramming:对 meta value 的普通 Python ``for`` 会在 parser 中展开。
+
+.. code-block:: python
+
+ n = T.meta_var(4) # n is a Python int, inlined
+ for j in range(n): # unrolled at parse time
+ acc[0] = acc[0] + A[tx, j]
+
+``@T.inline`` — inline function
+-------------------------------
+
+``@T.inline`` 定义一个函数,其 body 会在 parsing 阶段 **inline 到每个 call site**,生成代码中不会出现调用。它遵循 Python 的 lexical(LEGB)scope,并使用 late binding,因此参数会 shadow 外层变量:
+
+.. code-block:: python
+
+ @T.inline
+ def add_into(acc, x):
+ acc[0] = acc[0] + x
+
+ add_into(acc, A[tx, j]) # inlined -> acc[0] = acc[0] + A[tx, j]
+
+``@T.meta_class`` — parser-side state object
+--------------------------------------------
+
+``@T.meta_class`` 标记一个普通 Python class,其 **instance 是 parser meta value**:字段可以持有 buffer 和 scalar,因此你可以把相关 allocation 和 state 打包成一个对象,并在 kernel body 中使用它。
+
+.. code-block:: python
+
+ @T.meta_class
+ class State:
+ def __init__(self, smem):
+ self.acc = T.alloc_local([1], "float32")
+ self.buf = T.decl_buffer([64], "float16", smem, scope="shared.dyn")
+
+ s = State(smem.data)
+ s.acc[0] = T.float32(0.0) # use its fields like ordinary buffers
+ # ... s.buf[i] ...
+
+这很适合把 kernel 的 pipeline state(barrier、accumulator、scratch view)分组,而不是把许多独立 local 在线程中传来传去。
+
+``T.constexpr``
+---------------
+
+``T.constexpr`` 标记 compile-time kernel parameter,它会由 ``@T.jit`` 的 ``.specialize(...)`` 固化。细节见 :ref:`chap_tirx_primer`。
diff --git a/zh/tirx_guide/language_reference/cuda/threads_sync.rst b/zh/tirx_guide/language_reference/cuda/threads_sync.rst
new file mode 100644
index 00000000..24137b23
--- /dev/null
+++ b/zh/tirx_guide/language_reference/cuda/threads_sync.rst
@@ -0,0 +1,97 @@
+CUDA C++/PTX intrinsics
+=======================
+
+当没有 tile primitive 覆盖你需要的操作时,有两种 escape hatch 可以直接触达硬件:**调用 backend intrinsic**(来自 ``tvm.backend.cuda`` 的 ``T.cuda.*`` / ``T.ptx.*`` namespace),或者 **inline raw CUDA** 源码。
+
+Calling backend intrinsics
+--------------------------
+
+``T.cuda.*`` 和 ``T.ptx.*`` 直接暴露 CUDA backend 的 device intrinsic,包括 synchronization、mbarrier、reduction,以及 PTX data-movement / MMA 指令族:
+
+.. code-block:: python
+
+ T.cuda.cta_sync() # block barrier (__syncthreads)
+ T.cuda.warp_sync() # __syncwarp
+ T.cuda.warpgroup_sync(8) # warpgroup barrier
+ T.cuda.cta_sum(val, num_warps, scratch.ptr_to([0])) # block-level reduction
+
+ bar = T.alloc_shared((1,), "uint64")
+ T.ptx.mbarrier.init(bar.data, 1) # mbarrier for async completion
+ T.ptx.mbarrier.try_wait(bar.data, phase)
+
+一个完整可运行示例:通过 ``T.tvm_warp_shuffle_xor`` 做 warp all-reduce:
+
+.. code-block:: python
+
+ @T.prim_func
+ def warp_reduce(A_ptr: T.handle):
+ A = T.match_buffer(A_ptr, (32,), "float32", align=16)
+ T.device_entry()
+ cta_id = T.cta_id([1]); warp_id = T.warp_id([1]); lane_id = T.lane_id([32])
+ v = T.alloc_local((1,), "float32"); i = T.alloc_local((1,), "int32")
+ v[0] = T.float32(31 - lane_id)
+ i[0] = 16
+ while i[0] >= 1:
+ v[0] += T.tvm_warp_shuffle_xor(0xFFFFFFFF, v[0], i[0], 32, 32)
+ i[0] = i[0] // 2
+ A[lane_id] = v[0]
+
+Shuffle 会直接 lower 成 ``__shfl_xor_sync``:
+
+.. code-block:: c++
+
+ v_ptr[0] = v_ptr[0] + __shfl_xor_sync(0xFFFFFFFF, v_ptr[0], i_ptr[0], 32);
+
+``T.ptx.*`` / ``T.cuda.*`` 下还有其他指令族:``cp_async``(LDGSTS)、``cp_async.bulk.tensor``(TMA)、``ldmatrix`` / ``stmatrix``、``tcgen05.*``(Blackwell MMA)、``atomic_add``、``fence`` 等。完整 ``tvm.backend.cuda`` reference 请参阅 backend API reference。
+
+Synchronization semantics
+-------------------------
+
+GEMM 和 Flash Attention kernel 中反复出现四种同步机制。由于它们控制异步引擎和并行线程组,误用任何一种通常都会导致 silent corruption 或 deadlock。
+
+**Mbarrier Phase。** Mbarrier 用一个内部 phase bit 跟踪 arrival。``T.ptx.mbarrier.try_wait(bar, phase)`` 会阻塞,直到 barrier 的内部 phase *不同于* 调用方提供的 ``phase`` 参数。因此,跨 loop iteration 复用 barrier 时,调用方必须在每次 wait 后翻转自己的本地 phase tracker(``phase ^= 1``)。如果不这样做,后续 wait 会立即返回,允许 engine 读取半写入的 memory。:ref:`chap_gemm_basics` 给出了完整 phase-tracking 表。
+
+**Election。** ``T.ptx.elect_sync()`` 会在 *一个 warp 内的 active lane* 中选出一个,不一定是 lane 0,也不是每个 CTA 一个线程。要把 issuer 缩窄到恰好一个线程,必须配合 warp-level guard。:ref:`chap_gemm_basics` 中发起 ``Tx.gemm_async`` 和 ``tcgen05.commit`` 时使用的 pattern 是先 ``if warp_id == 0:``,再 ``if T.ptx.elect_sync():``。
+
+**Named Warpgroup Barrier。** ``T.cuda.cta_sync()`` 映射到 ``__syncthreads()``,需要 *每个* CTA thread arrive。一旦 warpgroup specialization 到不同 code path,把 ``cta_sync()`` 放在 warpgroup branch 内就会 deadlock,因为其他 warpgroup 永远到不了。硬件提供 16 个 named barrier(ID 0 到 15);``T.cuda.warpgroup_sync(10)`` 只同步一个 warpgroup 的线程。不同 warpgroup 使用不同 ID(例如 ``warpgroup_sync(wg_id + 10)``),避免撞到同一个硬件 barrier。见 :ref:`chap_gemm_advanced`。
+
+**Fence。** Fence 会把 producer 的写入排在 consumer(通常是异步 engine)读取之前:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 50 50
+
+ * - Fence
+ - 排序内容
+ * - ``T.ptx.fence.proxy_async("shared::cta")``
+ - 线程写入的 shared memory 在 async proxy(TMA store / MMA)读取之前可见
+ * - ``T.ptx.fence.mbarrier_init()``
+ - mbarrier initialization 在后续 arrival 或 wait 使用 barrier 之前完成
+ * - ``T.ptx.tcgen05.fence.after_thread_sync()``
+ - ``tcgen05`` writeback edge 上的保守 ordering fence(Steps 8 和 9 添加;TMA-to-MMA 路径不需要)
+
+Inlining raw CUDA
+-----------------
+
+对于完全没有 intrinsic 的操作,可以用 ``T.cuda.func_call(name, *args, source_code=..., return_type=...)`` 从源码字符串注入一个 ``__device__`` function:
+
+.. code-block:: python
+
+ SRC = r"""
+ __device__ __forceinline__ float my_relu(float x) { return x > 0.f ? x : 0.f; }
+ """
+
+ @T.prim_func
+ def k(A_ptr: T.handle, B_ptr: T.handle):
+ A = T.match_buffer(A_ptr, (256,), "float32")
+ B = T.match_buffer(B_ptr, (256,), "float32")
+ T.device_entry(); bx = T.cta_id([1]); tx = T.thread_id([256])
+ B[tx] = T.cuda.func_call("my_relu", A[tx], source_code=SRC, return_type="float32")
+
+源代码会原样 emit,调用也会连上:
+
+.. code-block:: c++
+
+ __device__ __forceinline__ float my_relu(float x) { return x > 0.f ? x : 0.f; }
+ // ...
+ B_ptr[tx] = my_relu(A_ptr[tx]);
diff --git a/zh/tirx_guide/language_reference/index.md b/zh/tirx_guide/language_reference/index.md
index 57068790..87255c21 100644
--- a/zh/tirx_guide/language_reference/index.md
+++ b/zh/tirx_guide/language_reference/index.md
@@ -1,10 +1,14 @@
----
-orphan: true
----
-
(chap_language_reference)=
# TIRx 语言参考
-> 翻译状态:待翻译。对应英文目录:`tirx_guide/language_reference/`。
+这是编写 TIRx device kernel 时可用的完整语言特性集合,从 {ref}`chap_tirx_primer` walkthrough 中拆出:parser utility、data type 和 expression、buffer 与 memory、control flow,以及 thread synchronization。需要精确语法或语义时,请查阅这些页面。
+
+```{toctree}
+:maxdepth: 1
-本页用于放置 TIRx 语言参考的中文内容。
+cuda/parser_utils
+cuda/data_types
+cuda/buffers
+cuda/control_flow
+cuda/threads_sync
+```