深入解析 TT-BUDA 架构及编译执行过程

引言

前文(探索 Tenstorrent 的 AI 开发软件栈:TT-BUDA)介绍了tt-buda的整体架构和frontend部分,本文主要介绍Netlist和backend部分。如下图所示,右半部分内容,重点解析Netlist格式,以及backend如何根据Netlist生成可执行程序和路由文件。


图 1:tt-buda software stack overview(来源:Tenstorrent)

Netlist分析

Netlist作为tt-buda软件栈的中间格式,连接了frontend和backend。frontend会对计算图进行优化,Netlist会包含优化后的图的信息,节点表示为 queueops,边表示为节点之间的连接。Netlist分成devices、queues、graphs、programs、test-config、fused_ops六个部分,其中,需要重点关心的是queuesgraphsprograms。为了对Netlist有个基本的印象,可以先观察一个简单的示例(netlist_softmax_single_tile.yaml):


图 2:Netlist 示例前半部分


图 3:Netlist 示例后半部分

devices描述了支持的设备,test-config描述测试数据(有了测试数据,就不需要frontend的runtime生成和传输数据到queue,单独的netlist就可以进行测试)。

queues 解析

字段 描述
input 队列的生产者,必须是 op 节点或HOST(表示主机CPU)。
type 队列类型,目前支持 ram(主存)和 queue(带写指针和读指针控制的 FIFO 队列)。
entries 条目数,通常是graph中input_count的倍数,参数 ram 或常量 queue 的条目数为 1。
grid_size 数据缓冲区的网格尺寸[rows, cols],每个缓冲区包含 entries 条目。网格中缓冲区总数为 grid_size[0] * grid_size[1],匹配 dramhost 中分配的缓冲区数量。
t 数据的“时间”维度,用于数据的时间流传输。对应tensor的depth维度。每个 t 切片是一个macroblock。
mblock 每个macroblock包含[rows, cols]个microblocks。
ublock 每个microblocks包含[rows, cols]个tiles。
ublock_order microblocks中tile的内存布局顺序,当前支持 r(行优先)或 c(列优先),默认值为 r
layout 描述底层内存中的数据布局。默认使用 tilized 布局,用户可以选择 flat 或其他自定义布局。
alias 仅用于 dual-view IO,将当前 IO 作为指定 IO 的别名。两个视图可以有不同的块方案,但必须有相同的条目大小和相同的底层内存(locdram/host 地址)。
df 队列中张量的数据格式,当前支持格式有 Float32Float16/Float16_b(bfloat16)、RawUInt32/RawUInt16/RawUInt8Bfp8/Bfp8_bBfp4/Bfp4_bBfp2/Bfp2_b
target_device 队列分配的目标设备 ID,设备内存类型由 loc 指定。
loc 队列的位置,目前支持 dramhost
dram 仅当 locdram 时有效,指定分配 queue/ram 的 dram bank和bank的本地地址。分配的数量和顺序与 grid_size 中的缓冲区数量和顺序匹配(行优先顺序)。
host 仅当 lochost 时有效,指定分配 queue/ram 的主机地址。分配的数量和顺序与 grid_size 中的缓冲区数量和顺序匹配(行优先顺序)。

queue 是FIFO队列,用于保存输入、输出、常量和权重等信息。t(times)、mblock、ublock 是数据传输的三个维度。

graphs 解析

graphs可以包含多个epoch,每个epoch包含多个op。同一个epoch中的ops不可复用tensix,不同的epoch的tensix可能对应不同的op,所以不同的epoch可能需要更换tensix上的可执行文件和路由文件。

op的字段解析如下:

字段 描述
type 当前操作的计算类型,用于确定操作所使用的计算内核。每种计算类型支持不同的属性集,不同数量的输入和输出。
grid_loc 标记网格左上角的坐标位置。网格大小由 grid_size 决定。
grid_size 分配给当前操作的 Tensix 核心网格[rows, cols]。网格中的核心总数为 grid_size[0] * grid_size[1]。同一图中的两个操作不能共享相同的核心,即不能有重叠的网格。
inputs 当前操作的输入操作数,必须是 queue/ram 或其他 op。如果输入是另一个 op,则当前操作是输入操作输出的消费者。如果输入是 queue/ram,则当前操作是主动读取器。
input_x_tms 输入操作数 x 的张量操作(tensor manipulation TM)规范。TM 操作在节点之间通过管道的数据传输路径上实现。管道和实现细节不在网络列表范围内,由 BUDA 后端处理。
input_buf_min_size_tiles 输入操作数缓冲区的最小大小,以 tile 为单位,如果未指定,由 BUDA 后端确定大小。
t 数据的“时间”维度,用于数据的时间流传输。对应tensor的depth维度。每个 t 切片是一个macroblock。
mblock 每个macroblock包含[rows, cols]个microblocks。
ublock 每个microblocks包含[rows, cols]个tiles。
buf_size_mb 输出缓冲区的大小,以macroblock为单位,如果未指定,默认值为 1。
ublock_order microblocks中tile的内存布局顺序,当前支持 r(行优先)或 c(列优先),默认值为 r
in_df 输入操作数的数据格式。元素数量与 input 中指定的输入操作数数量匹配。每个元素的数据格式应与其对应的 input 操作数的数据格式匹配。
out_df 输出操作数的数据格式。每个元素的数据格式应与输出 queue/ram 的或消费者操作数的数据格式匹配。
intermed_df 中间操作数的数据格式。
acc_df Tensix 核心数学单元的目标寄存器累加器使用的数据格式。
math_fidelity 操作的数学精度,精度越高结果越准确,但所需的计算周期也越多。目前支持的精度级别从低到高依次为 LoFiHiFi2HiFi3HiFi4
attributes 特定于操作计算 type 的属性映射。
untilize_output 一个布尔标志,指定是否应取消操作输出的 tiling。如果设置为 true,则所有设备特定的 tiling 和阻塞都将被移除,最终输出与 torch tensor 中的布局相同。这通常用于最终输出回到主机 python/pybuda 运行时。如果未指定,默认值为 false
grid_transpose 一个布尔标志,指定操作网格是否应转置。如果设置为 true,操作网格将转置,即行和列互换。如果未指定,默认值为 false
gradient_op 一个布尔标志,指定操作是否为梯度累积操作。如果未指定,默认值为 false

其中,grid_loc和grid_size用于描述一个op分配到哪几个Tensix进行运算。


图4: grid_loc: [1,1] 和 grid_size: [2,3] 表示的 Tensix 网格

如图4是一个4x4的Tensix网格,每个网格代表一个Texsix,Tensix 的 grid_loc 为网格左上角的坐标。grid_loc: [1,1] 和 grid_size: [2,3] 表示从坐标[1,1]开始,由[1,1]、[1,2]、[2,1]、[2,2]、[1,3]、[2,3]组成的大小为6的 Tensix 网格, 即绿色的6个网格。

数据的流动

queues 和 ops 都是图的节点,它们之间的数据传输是图的边。如下图所示,数据的流动顺序如下:

  1. 从 input_activation(dram) 流向 exp(tensix [0,0])


图 5:softmax_single_tile 的数据流动1

  1. 从 exp(tensix [0,0]) 和 input_constant(dram) 流向 sum(tensix [0,1])


图 6:softmax_single_tile 的数据流动2

  1. 从 sum(tensix [0,1]) 流向 recip(tensix [0,2])


图 7:softmax_single_tile 的数据流动3

  1. 从 exp(tensix [0,0]) 和 recip(tensix [0,2]) 流向 mult(tensix [0,3])


图 8:softmax_single_tile 的数据流动4

  1. 从 mult(tensix [0,3])流向 output_softmax(dram)(output_softmax的input字段显示)


图 9:softmax_single_tile 的数据流动5

programs 解析

programs 由一系列的代码序列组成, 提供了一套语法可以定义常量、变量、循环、图的执行、队列的设置。如图3所示,定义了一个循环,循环执行所有的图。

fused_ops

fused_ops 是一种特殊的op,用于将多个op融合成单个op,减少图的op数量,减少tensix之间的数据传输。每个fused_op有唯一的id,要想使用fused_op,需要将op的type标记为fused_op,并将id放到attributes中。使用示例如下:

fused_ops:
  0:
    inputs: 2
    intermediates: 2
    schedules: 
      -
        - add: { type: add, inputs: [input0, input1], mblock: [2, 2], ublock: [2, 2], output: intermed0}
        - reciprocal: { type: reciprocal, inputs: [intermed0], mblock: [2, 2], ublock: [2, 2], output: intermed1}
        - sqrt: { type: sqrt, inputs: [intermed0], pop: [intermed0], mblock: [2, 2], ublock: [2, 2], output: dest}
        - exp: { type: exp, inputs: [dest], mblock: [2, 2], ublock: [2, 2], output: dest}
        - datacopy: { type: datacopy, inputs: [dest], mblock: [2, 2], ublock: [2, 2], output: intermed0}
        - mul: { type: multiply, inputs: [intermed1, intermed0], pop: [intermed0, intermed1], mblock: [2, 2], ublock: [2, 2], output: output}

声明了一个fused_op,id为0,融合了6个op,使用方式如下:

    _fused_op_0: {type: fused_op, grid_loc: [0, 1], grid_size: [1, 1], inputs: [q1, matmul],
         t: 1, mblock: [2, 2], ublock: [2, 2], buf_size_mb: 2, ublock_order: r, in_df: [Float16_b, Float16_b], out_df: Float16_b, intermed_df: Float16_b, acc_df: Float16_b, math_fidelity: HiFi3,
         attributes: {fused_op_id: 0}}

使用 attributes: {fused_op_id: 0} 标识 fused_op 的id。

Budabackend

目录结构

如图1所示,budabackend 是后端多个工具和库的集合。budabackend 项目源码目录如下图所示:


图 10:budabackend项目 源码 目录结构

源码目录有点凌乱,包含了很多辅助文件(如性能分析和测试、文档、配置文件等等),除此之外,较为重要的有:

  • netlist/netlist_analyzer – 解析和分析Netlist

  • ops – op的源码实现,每个type的op都有自己的一份实现

  • hlks – high level kernels 每个op都可以映射到hlk

  • third_party/tt_llk_grayskull,third_party/tt_llk_wormhole_b0 – low level kernels 实现

  • src/net2pipe,src/pipegen2 – 路由文件生成工具,解析netlist生成noc overlay blob文件

  • third_party/sfpi – 自定义riscv工具链,将c++源码编译成riscv可执行文件

  • runtime/loader – 调用其他工具,编译和加载

编译与执行

如图1所示,编译可以分成两条路径执行,分别生成二进制文件和路由文件(编译生成的文件详见上篇教程)。

二进制文件的生成

netlist解析器会遍历分析graphs中的每个op,op的type是一个字符串,解析器会将op.type映射到一个class tt_op 的子类(tt_op子类实现在ops目录下),每个子类会有自己的规则对应到hlk(hlks目录下的一个具体的文件),hlk可以下降到llk(low level kernel),然后调用自定义工具链将源文件编译成可执行文件。

使用 softmax_single_tile.sum 作为示例说明:

sum:   {type: matmul,     grid_loc: [0, 1], grid_size: [1, 1], 
        inputs: [exp, input_constant], t: 1, mblock: [1, 1], 
        ublock: [1, 1], in_df: [Float16_b, Float16_b], 
        acc_df: Float16, out_df: Float16_b, intermed_df: Float16_b, 
        ublock_order: r, buf_size_mb: 2, math_fidelity: HiFi3, 
        attributes: {m_k: 1, u_kt: 1}}

可以看出 type 是 matmul(矩阵乘),netlist 解析器将 “matmul” 映射到 class tt_mm_bare_op

bool is_valid_matmul_op(string matmul_op) { return (matmul_op == "matmul"); }

std::shared_ptr<tt_op> netlist_utils::create_op(
    tt_op_info* op_info_ptr, const unordered_map<string, 
    tt_fused_op_info>& fused_ops_map) {
    ...
    if (is_valid_matmul_op(op_info_ptr->type)) {
        new_tt_op = std::static_pointer_cast<tt_op>(
            std::make_shared<tt_mm_bare_op>(...));
    }
    ...
    return new_tt_op;
}

查看 tt_mm_bare_op 的实现,可以看出,matmul op 对应了 hlks/matmul/matmul_*.cpp(不同的数据格式有不同实现)

tt_mm_bare_op::tt_mm_bare_op(...) {
    ...
    set_hlk_cpp_file_name_all_cores("hlks/" + 
       get_hlks_file_name_no_extension(identity, int_fpu_en) + ".cpp");
    ...
}

op对应的源文件目录如下图所示:


图 11:op 源文件目录

其中,hlks/matmul/matmul_u.cpp 被重命名为 hlks.cpp (这个文件名是固定的),是op的具体实现(hlk和llk的具体实现请见下期教程)。链接命令如下:

./third_party/sfpi/compiler/bin/riscv32-unknown-elf-g++ -mgrayskull 
   -march=rv32iy -mtune=rvtt-b1 -mabi=ilp32 -flto -ffast-math -O3 
   -fno-exceptions -Wl,--gc-sections -Wl,-z,max-page-size=16 
   -Wl,-z,common-page-size=16 -Wl,--defsym=__firmware_start=0 
   -Wl,--defsym=__trisc_base=53248 -Wl,--defsym=__trisc0_size=20480 
   -Wl,--defsym=__trisc1_size=16384 -Wl,--defsym=__trisc2_size=20480 
   -Wl,--defsym=__trisc_local_mem_size=4096 
   -Tsrc/firmware/riscv/toolchain/trisc1.ld 
   -Lsrc/firmware/riscv/toolchain -nostartfiles 
   -o 
   ./tt_build/test/graph_softmax_single_tile/op_0/tensix_thread1/tensix_thread1.elf 
   ./tt_build/test/graph_softmax_single_tile/op_0/tensix_thread1/substitutes.o 
   ./tt_build/test/graph_softmax_single_tile/op_0/tensix_thread1/tmu-crt0.o 
   ./tt_build/test/graph_softmax_single_tile/op_0/tensix_thread1/ckernel_unity.o

值得注意的是,arch是rv32iy,不含浮点指令(Grayskull 与 Wormhole 中的 Tensix RISC-V 核不支持浮点,但Blackhole 支持浮点)。使用了指定的链接脚本 trisc1.ld,部分内容如下:

MEMORY
{
    LOCAL_DATA_MEM : ORIGIN = 0xFFB00000, LENGTH = 4K
    LOCAL_DATA_MEM_TRISC : ORIGIN = 0xFFB00000, LENGTH = 2K
    LOCAL_DATA_MEM_NOCRISC : ORIGIN = 0xFFB00000, LENGTH = 4K
    L1 : ORIGIN = 0x00000000, LENGTH = 512K
    BRISC_CODE : ORIGIN = 0x00000000, LENGTH = 8448
    ZEROS : ORIGIN = 0x00002100, LENGTH = 512
    ETH_L1 : ORIGIN = 0x00000000, LENGTH = 256K
    /* CKernel code region
     = CA::KERNEL_DEFAULT_SIZE_PER_CORE */
    TRISC0_CODE : ORIGIN = __trisc_base, LENGTH = DEFINED(__trisc0_size) ? DEFINED(__trisc_local_mem_size) ? __trisc0_size - __trisc_local_mem_size : __trisc0_size : 16K
    TRISC1_CODE : ORIGIN = DEFINED(__trisc0_size) ? __trisc_base + __trisc0_size : 0x0000AC00, LENGTH = DEFINED(__trisc1_size) ? DEFINED(__trisc_local_mem_size) ? __trisc1_size - __trisc_local_mem_size : __trisc1_size : 16K
    TRISC2_CODE : ORIGIN = DEFINED(__trisc1_size) ? __trisc_base + __trisc0_size + __trisc1_size : 0x0000EC00, LENGTH = DEFINED(__trisc2_size) ? DEFINED(__trisc_local_mem_size) ? __trisc2_size - __trisc_local_mem_size : __trisc2_size : 16K
    /* Ckernel migrated code region
     = CA::KERNEL_DEFAULT_SIZE_PER_CORE */
    TRISC0_OVRD_CODE : ORIGIN = 0x000F4000, LENGTH = 16K
    TRISC1_OVRD_CODE : ORIGIN = 0x000F8000, LENGTH = 16K
    TRISC2_OVRD_CODE : ORIGIN = 0x000FC000, LENGTH = 16K
    NOCRISC_DATA : ORIGIN = 0x33000, LENGTH = 4K
    NOCRISC_CODE : ORIGIN = 0x5000, LENGTH = 16K
    NOCRISC_L1_CODE : ORIGIN = 0x9000, LENGTH = 16K
    NOCRISC_L1_SCRATCH : ORIGIN = 0x33200, LENGTH = 3584
    ERISC_CODE : ORIGIN = 0x00000000, LENGTH = 16K
    ERISC_DATA : ORIGIN = 0x00004000, LENGTH = 4K
    ERISC_APP_DATA : ORIGIN = 0x0003F000, LENGTH = 4K
}

可以看出,因为 Grayskull Tensix 中的5个 RISC-V 核共用 1M L1 SRAM,不同的核是分开编译的,但是共用了一部分链接脚本,Code与Data会按顺序排布在L1上(更详细的内存分布信息见源文件src/firmware/riscv/grayskull/l1_address_map.h)。

路由文件的生成

路由文件是指图1所示的 NOC overlay blob,是硬件 NOC overlay 的一种配置文件。NOC overlay 层可以隔离计算层和NOC层,计算核不再需要关心数据移动,专注于计算即可(与tt-buda不同,tt-metal需要手动调用NOC api进行数据移动,后期tt-metal教程中会详细说明)。

runtime 会调用工具 net2pipe、pipegen,为了复现路由文件的生成过程,手动调用工具生成 blob 文件。

# net2pipe 生成 pipegen.yaml
./build/bin/net2pipe  verif/graph_tests/netlists/netlist_softmax_single_tile.yaml 
    ./tt_build/test 0 ./tt_build/test/device_desc.yaml

# pipegen2 生成 blob.yaml
build/bin/pipegen2 tt_build/test/temporal_epoch_0/overlay/pipegen.yaml 
    tt_build/test/device_desc.yaml 
    tt_build/test/temporal_epoch_0/overlay/blob.yaml 0 0

# blobgen 生成 pipegen_epoch*.hex
ruby ./src/overlay/blob_gen.rb --blob_out_dir 
    ./tt_build/test/temporal_epoch_0/overlay/
    --graph_yaml 1 --graph_input_file 
    tt_build/test/temporal_epoch_0/overlay/blob.yaml 
    --graph_name pipegen_epoch0 --root ./ --noc_x_size 13 --noc_y_size

可以看出,blob文件生成分成三步,有两个重要的中间文件:pipegen.yaml 和 blob.yaml。

  • pipegen.yaml – 描述了buffer和pipe的信息,pipe是多个buffer(两个或以上)之间的连接

  • blob.yaml – 将buffer和pipe的信息重新组装成 stream,包含源地址、目标地址和坐标等等信息

针对每个 tensix core 生成一个 overlay blob 文件,命名为 #{graph_name}#{chip_id}#{y}_#{x}.hex,在加载过程中,blob文件会被放在地址OVERLAY_BLOB_BASE处(在l1_address_map中描述)。

总结

本文详细介绍了Netlist的格式,以及使用tt-buda后端进行编译和执行的过程。Netlist作为承上启下的中间格式,描述了算子和Tensix的映射关系、数据流动的方向等关键信息。tt-buda后端解析Netlist,编译生成运行所需的二进制文件和路由文件。通过分析整个解析和编译的过程,可以了解计算图(由算子组成)是如何在Tenstorrent硬件上运行起来的。

在后续的教程中,我们将更详细地介绍算子的不同层次实现,从高层次算子库(HLK)到低层次算子库(LLK),以及自定义指令等。通过解析算子的实现,有助于理解计算单元的原理,并学习如何定义和实现自己的算子。

参考资料:

【1】budabackend github 项目地址 GitHub - tenstorrent/tt-budabackend: Buda Compiler Backend for Tenstorrent devices

【2】tenstorrent NOC overlay 专利说明 CN112306946A - 用于处理器核心的网络的覆盖层 - Google Patents