【免费下载链接】Booth
Open-source CUDA, Triton and HIP compiler targeting multiple GPU and CPU architectures.
Booth是一款开源的 CUDA、Triton 与 HIP 编译器,能把熟悉的 GPU 内核源码直接编译到 AMD、NVIDIA 以及 Tenstorrent 数据流 GPU 等多种架构。它内部有一层叫TDF(Tile DataFlow,瓦片数据流)的中间表示,专门负责把一颗 CUDA 内核拆分成region(区域)、channel(通道)与 arc(弧),并编排 Tenstorrent 芯片上的 NoC 片上网络与 L1 循环缓冲区。本文带你读懂这套编排机制。
为什么用「港口」打比方?因为 region 就是一个个码头,channel 是码头之间的传送带,NoC 是连接码头的航道。数据瓦片(tile)在它们之间流转,和港口装卸货物的节奏一模一样。
为什么需要 TDF 层:编译器里的「中间层」
Tenstorrent 的 GPU 和 NVIDIA/AMD 很不一样:它不是靠成千上万个线程并行,而是把任务摊在多个「Tensix」单元上,每个单元由几个 RISC-V 小核心分工协作。这就没法直接套用传统的「一个 kernel 跑满所有线程」的思路。
Booth 为此在核心 IR(BIR)之上加了 TDF 这一层。一句话概括它的职责:
把一个
__global__CUDA 内核,变成一张「区域通过通道通信」的图,再落到具体硬件上。
- 对AMD / NVIDIA,这张图会「塌缩」成一个区域,原样交给已有后端,几乎零开销。
- 对Tenstorrent(Tensix),图会展开成「一个 baby core 一个区域」,通道变成 L1 里的循环缓冲区,弧变成内联的 NoC + CB(循环缓冲)操作。
这一层的设计词汇刻意借鉴了大型机世界:区域像 CICS 事务、通道像 TPF 循环块表、弧像由信号量强制执行的 ENQ/DEQ 配对。这些模式 1968 年就用来跑航空订票系统,如今搬到了 GPU 上。更多背景见 docs/mainframe.md。
三大核心概念:Region、Channel、Arc
TDF 的全部家当就是下面三样东西,它们一起构成一张数据流图。定义都集中在 tdf.h 中。
| 概念 | 结构体 | 一句话解释 | 类比港口 |
|---|---|---|---|
| Region 区域 | td_rgn_t | 一个带角色的小程序,绑定一个 baby core,有自己的 L1 工作区 | 一个码头 |
| Channel 通道 | td_chan_t | 两个区域间的循环缓冲区,带深度与瓦片形状 | 码头间的传送带 |
| Arc 弧 | td_arc_t | 区域在某个点发出的一次同步/搬运操作 | 传送带上的一次装卸动作 |
Region:三种角色各司其职
一个区域是一个「事务性程序」,带一个角色、绑定一个 baby core,还有一块 L1 工作存储(TWA)。Tensix 的标准角色有三种(外加一个用于非 Tenstorrent 后端的 SOLO):
- RDR(reader,读取者):负责 NoC → L1,把数据从显存搬进本地缓存。
- CMP(compute,计算):在 T0/T1/T2 核心上做真正的 FPU/SFPU 计算。
- WRT(writer,写入者):负责 L1 → NoC,把结果搬回显存。
- SOLO(单区):AMD/NVIDIA 后端的退化形态,整个内核就是一个区域。
这种「读—算—写」分工正是 Tenstorrent 数据流 GPU 的精髓:搬运和计算由不同核心流水线式地同时进行,谁也不用等谁。
Channel:带「标签」的循环缓冲区
通道是连接两个区域(生产者 → 消费者)的环形缓冲,depth表示环里一次能放几个瓦片,l1_off字段由放置阶段稍后填上。
每条通道都带一个tag(标签),像 Burroughs 描述符一样打包了瓦片的三要素:
- 形状:
rows × cols(默认 32×32) - 数据类型:fp32 / bf16 / int 等
- 布局:跨 DRAM bank 交织、按某轴分片、或固定在某块 L1
这样一旦生产者和消费者的形状/类型对不上,模块构建时就会触发断言,而不是运行期静默错乱。
Arc:六种「动作」
一条弧就是某区域在 body 里某个位置要发出的一个 CB 或 NoC 操作。它记录了来源的 BIR 指令下标(bir_inst锚点),以便把同步调用插到区域指令流的正确位置。共有六种,分成两类(见 tdf.h):
| 弧类型 | 对应操作 | 作用 |
|---|---|---|
RSV | cb_reserve_back | 生产者等待有空闲槽位 |
PUSH | cb_push_back | 生产者把瓦片提交进通道 |
WAIT | cb_wait_front | 消费者阻塞直到瓦片就绪 |
POP | cb_pop_front | 消费者把槽位还给生产者 |
RD | noc_async_read | NoC 异步读(显存 → L1) |
WR | noc_async_write | NoC 异步写(L1 → 显存) |
前四种是「生产者/消费者」的握手信号,后两种跨 L1/显存边界。这就是整条数据流水线的「节拍器」。
从 CUDA 到三分裂:fission 如何拆解内核
fission(裂变)是 TDF 层最关键的变换,源码在 tdf_fission.c。它把一个「单区 SOLO」模块,原地改写成 RDR/CMP/WRT 三区流水线:
- 分类指针参数:遍历内核的每条 load/store,把地址追溯回它来自哪个参数,标成「输入(被读)」或「输出(被写)」。
- 为每个输入参数建一条
RDR→CMP通道,并配上RD / PUSH / WAIT三条弧——读取者从显存拉瓦片,推送给计算区。 - 为每个输出参数建一条
CMP→WRT通道,配PUSH / WAIT / WR三条弧——计算区把结果推进通道,写入者再经 NoC 排回显存。
举个典型例子:矩阵乘法matmul(C, A, B, ...)有两个读参数(A、B)和一个写参数(C)。裂变后得到3 条通道、9 条弧(每个通道 3 条弧)。这个形状在测试 tests/ttdf.c 里被逐项断言,连noc0 len=4096、noc1 len=4096这样的字节数都精确校验。
一个参数如果既被读又被写(比如
saxpy的x = a*x + y原地更新),就会同时拿到进、出两条通道。
L1 放置:循环缓冲区怎么排进缓存
有了图和通道,还得决定每个通道在 L1 里放哪儿。这由placement(放置)路径完成,见 tdf_place.c。
Wormhole B0 的 L1 共1464 KiB,从0x00000000开始,组织成 16 个 bank。整块 L1 的布局像切蛋糕:
- 前 32 KiB:留给代码 + 栈(
TD_L1_CODE_RSV)。 - 再往下 256 B:runtime args 区,宿主在 dispatch 前写入 CUDA 坐标内建量(
threadIdx等)和标量参数,布局契约在 rt_args.h。 - CB 区:各通道的瓦片数据 + 8 字节 FIFO 头,从
TD_L1_CB_BASE向上按声明顺序「首次适配」打包,每段都按16 字节对齐(NoC 读写 L1 要求 C16 对齐,否则瓦片会悄悄错位损坏)。 - 顶部 64 KiB:
__shared__区,从 L1 顶端向下生长。它和 CB 区之间用td_shbase()隔开,谁也不能越界。
如果通道装不下(比如深度失控),放置会直接报错拒绝,而不是默默溢出。默认瓦片是 32×32 fp32 =4096 字节,这个数会一路影响到 NoC 对齐、CB 深度和 runtime args。
NoC 编排:读走哪条、写走哪条
芯片上有两条物理 NoC,流向相反。编排路径 tdf_noc.c 对每条RD/WR弧做两件机械但关键的事:
- 选网络:读走NoC 0(它的流向正好契合 Tensix→显存的读取方向),写走NoC 1(反向类比)。这是依据 Tenstorrent 路由文档得出的经验规则。
- 算长度:每次传输的字节数 = 瓦片字节数,并拒绝超过 8 KiB(
TD_NOC_MAX_XFER)的单次请求。更大的传输要等一个「切分 pass」,在那之前宁可大声失败,也不静默截断——「响亮的失败胜过安静的损坏」。
它同时暴露一个64 位 NoC 地址编码器td_noc_addr:把 36 位本地地址放在低 36 位,6 位 X 坐标放[41:36],6 位 Y 坐标放[47:42],高位保留。这套位布局直接来自 Wormhole 的 NoC 内存映射规范。
从 IR 到 RISC-V:弧如何落地成指令
最后,弧要被翻译成 baby core 能执行的 RISC-V 指令:
- CB 弧发射器
td_emit_cb_arc把RSV/WAIT/PUSH/POP降成 RISC-V 原语:等待是本地自旋,信号则是对对方 L1 里某个计数器做 NoC 原子自增。 - 搬运循环
td_emit_dma_loop则是读取者/写入者内核的「机器码形态」:从 runtime args 区读出显存基址和瓦片数,然后逐瓦片循环——先做 CB 门控(等待空槽/等数据),经 NoC 传输一块瓦片,等 barrier 落定,再推送/释放槽位,最后推进指针并在到达环尾时回卷到环首。这个回卷动作,正是「循环缓冲区在干活」。
这些 RISC-V 原语都封装在 src/tensix/noc.h,Tensix 后端在 src/tensix/ 里消费它们,最终产出 reader/compute/writer 三份 baby-core ELF。
实战:一行命令查看 TDF 布局
想亲眼看看自己内核的 TDF 图,不用翻源码,编译二进制造成的kath就有现成开关(完整命令参考见 docs/usage.md):
# 打印 TDF 布局:regions、channels、NoC arcs ./kath --tdf kernel.cu # 再进一步,跑完 fission 裂变,看三区流水线 ./kath --tdf-fission kernel.cu输出是纯文本、一行一项的人类可读形式(td_dump),比如:
TDF module (target=TENSIX) regions: 3 channels: 3 arcs: 9 region 0: RDR core=B x=0 y=0 region 1: CMP core=T0 region 2: WRT core=NC chan 0: rgn0 -> rgn1 tile 32x32 fINTRL depth=2 l1=0x8100 arc 2: rgn1 PUSH chan0 cnt=1 @bir[11]这张「地图」让你一眼看清:数据从哪个区域流到哪个区域、走哪条 NoC、每跳多少字节——排查数据流问题时非常有用。
关键源码与文档索引
想深入,按下面这张表顺藤摸瓜即可:
| 想了解什么 | 去哪里看 |
|---|---|
| TDF 层总体设计、常量与位布局 | src/tdf/tdf.h |
| 建图 API、查询与 dump | src/tdf/tdf.c |
| 目标感知的 lowering(单区塌缩 / 多区展开) | src/tdf/tdf_lower.c |
| 内核裂变:指针分类 + 三区重建 | src/tdf/tdf_fission.c |
| L1 放置:通道打包与预算 | src/tdf/tdf_place.c |
| NoC 编排、地址编码、CB/DMA 发射 | src/tdf/tdf_noc.c |
| baby-core NoC 传输与信号量原语 | src/tensix/noc.h |
| runtime args 布局契约 | src/tensix/rt_args.h |
| Tensix 后端常量与 SFPU 指令 | src/tensix/tensix.h |
| TDF 形状与集成测试(39 项) | tests/ttdf.c |
| 大型机风味特性(含 TDF 简介) | docs/mainframe.md |
小结
Booth 的 TDF 层是它理解 Tenstorrent 数据流 GPU 的「翻译官」。核心就三样:region划分角色、channel承载瓦片、arc控制节拍。配合fission 裂变、L1 放置和NoC 编排三步,一颗普通的 CUDA 内核就能被拆成读—算—写流水线,稳稳落到 RISC-V baby core 上。对想搞懂「数据流 GPU 到底怎么把数据搬来搬去」的人来说,./kath --tdf就是最好的入门窗口——先看这张图,再读源码,水到渠成。
【免费下载链接】Booth
Open-source CUDA, Triton and HIP compiler targeting multiple GPU and CPU architectures.
相关推荐
EdgeTranslate通信机制揭秘:Channel类如何协调前后端数据交互
EdgeTranslate通信机制揭秘:Channel类如何协调前后端数据交互 EdgeTranslate作为一款优秀的浏览器翻译扩展,其强大的通信机制是确保用
前端TileLang 三层编程接口与编译流程解析:Tile 化编程模型如何落地为多后端 GPU Kernel
TileLang 三层编程接口与编译流程解析:Tile 化编程模型如何落地为多后端 GPU Kernel 本文围绕 TileLang 官方的入门总览文档 doc
编译器编程语言高性能计算人工智能深度学习揭秘tiny-gpu:如何通过数据压缩技术优化GPU内存带宽
揭秘tiny gpu:如何通过数据压缩技术优化GPU内存带宽 在当今GPU性能瓶颈中,内存带宽优化已成为提升计算效率的关键所在。 tiny gpu 作为一个极简
硬件开发图形学教育
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考