大数跨境
0
0

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

深入解析 TT-BUDA 架构及编译执行过程 1nfinite 无限智域
2024-06-19
2

引言

前文探索 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 每个网格代表一个 Texsix,该 Tensix 的 grid_loc 为网格左上角的坐标。
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 可表示从坐标[1,1]开始,由[1,1]、[1,2]、[2,1]、[2,2]、[1,3]、[2,3]组成的大小为 6 的 Tensix 网格。

数据的流动

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: [01], grid_size: [11], inputs: [q1, matmul],
         t: 1, mblock: [22], ublock: [22], 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 项目地址 https://github.com/tenstorrent/tt-budabackend

[2] tenstorrent NOC overlay 专利说明 https://patents.google.com/patent/CN112306946A/zh?assignee=tenstorrent&oq=tenstorrent


--------END--------




点击阅读原文 加入1nfinite

【声明】内容源于网络
0
0
1nfinite 无限智域
RISC-V 和 AI 开源技术社区,欢迎加入我们一起探索新算力时代的未来。
内容 26
粉丝 0
1nfinite 无限智域 RISC-V 和 AI 开源技术社区,欢迎加入我们一起探索新算力时代的未来。
总阅读0
粉丝0
内容26