☰
Booth TDF Tile DataFlow层揭秘:Tenstorrent数据流GPU如何编排region/channel/NoC
2026/10/11 10:47:17 网站建设 项目流程

【免费下载链接】Booth

Open-source CUDA, Triton and HIP compiler targeting multiple GPU and CPU architectures.

项目地址:https://gitcode.com/gh_mirrors/bar/Booth
点击查看免费下载

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):

弧类型对应操作作用
RSVcb_reserve_back生产者等待有空闲槽位
PUSHcb_push_back生产者把瓦片提交进通道
WAITcb_wait_front消费者阻塞直到瓦片就绪
POPcb_pop_front消费者把槽位还给生产者
RDnoc_async_readNoC 异步读(显存 → L1)
WRnoc_async_writeNoC 异步写(L1 → 显存)

前四种是「生产者/消费者」的握手信号,后两种跨 L1/显存边界。这就是整条数据流水线的「节拍器」。

从 CUDA 到三分裂:fission 如何拆解内核

fission(裂变)是 TDF 层最关键的变换,源码在 tdf_fission.c。它把一个「单区 SOLO」模块,原地改写成 RDR/CMP/WRT 三区流水线:

  1. 分类指针参数:遍历内核的每条 load/store,把地址追溯回它来自哪个参数,标成「输入(被读)」或「输出(被写)」。
  2. 为每个输入参数建一条RDR→CMP通道,并配上RD / PUSH / WAIT三条弧——读取者从显存拉瓦片,推送给计算区。
  3. 为每个输出参数建一条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弧做两件机械但关键的事:

  1. 选网络:读走NoC 0(它的流向正好契合 Tensix→显存的读取方向),写走NoC 1(反向类比)。这是依据 Tenstorrent 路由文档得出的经验规则。
  2. 算长度:每次传输的字节数 = 瓦片字节数,并拒绝超过 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、查询与 dumpsrc/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.

项目地址:https://gitcode.com/gh_mirrors/bar/Booth
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询