VTA硬件
VTA硬件
提供了VTA硬件设计的自上而下的概述。本硬件设计涵盖两个级别的VTA硬件:
- VTA设计及其ISA硬件-软件接口的体系结构概述。
- VTA硬件模块的微体系结构概述以及计算核心的微代码规范。
VTA概述
VTA是为快速,高效的密集线性代数而构建的通用深度学习加速器。VTA集成了一个简单的类似RISC的处理器,可以对1或2级张量寄存器执行密集的线性代数运算。另外,该设计采用解耦访问执行以隐藏内存访问延迟。
在更广泛的范围内,VTA可以用作模板的深度学习加速器设计,以进行完整的堆栈优化,从而将通用张量计算接口公开给编译器堆栈。
本文从总体上概述了VTA硬件组织。VTA由四个模块组成,这些模块通过FIFO队列和本地内存块(SRAM)相互通信,以实现任务级管道并行性:
- 提取模块负责从DRAM加载指令流。解码这些指令,路由到三个命令队列之一。
- 加载模块负责将来自DRAM的输入和权重张量加载到数据专用的片上存储器中。
- 计算模块使用GEMM核执行密集线性代数计算,并使用张量ALU进行常规计算。将数据从DRAM加载到寄存器文件中,以及将micro-op内核加载到micro-op缓存中。
- 存储模块将计算核心产生的结果存储回DRAM。
HLS硬件来源组织
VTA设计当前在Vivado HLS C ++中指定,只有Xilinx工具链才支持。VTA硬件来源包含在3rdparty/vta-hw/hardware/xilinx/sources:
- vta.cc 包含每个VTA模块的定义以及顶级VTA设计的顶级行为模型。
- vta.h包含使用Xilinxap_int类型的类型定义以及函数原型声明。
预处理器宏在下定义3rdparty/vta-hw/include/vta/hw_spec.h。这些宏定义中的大多数是从3rdparty/vta-hw/config/vta_config.json文件中列出的参数派生的。通过处理json文件3rdparty/vta-hw/config/vta_config.py来生成一串编译标志,这些编译标志定义了预处理器宏。makefile使用该字符串,以便在HLS硬件综合编译器和构建VTA运行时的C ++编译器中设置那些高级参数。
HLS模块示例
显示了C ++中定义的VTA模块之一的定义:
void fetch(
uint32_t insn_count,
volatile insn_T *insns,
hls::stream<insn_T> &load_queue,
hls::stream<insn_T> &gemm_queue,
hls::stream<insn_T> &store_queue) {
#pragma HLS INTERFACE s_axilite port = insn_count bundle = CONTROL_BUS
#pragma HLS INTERFACE m_axi port = insns offset = slave bundle = ins_port
#pragma HLS INTERFACE axis port = load_queue
#pragma HLS INTERFACE axis port = gemm_queue
#pragma HLS INTERFACE axis port = store_queue
#pragma HLS INTERFACE s_axilite port = return bundle = CONTROL_BUS
INSN_DECODE: for (int pc = 0; pc < insn_count; pc++) {
#pragma HLS PIPELINE II = 1
// Read instruction fields
insn_T insn = insns[pc];
// Do some partial decoding
opcode_T opcode = insn.range(VTA_INSN_MEM_0_1, VTA_INSN_MEM_0_0);
memop_id_T memory_type = insn.range(VTA_INSN_MEM_5_1, VTA_INSN_MEM_5_0);
// Push to appropriate instruction queue
if (opcode == VTA_OPCODE_STORE) {
store_queue.write(insn);
} else if (opcode == VTA_OPCODE_LOAD &&
(memory_type == VTA_MEM_ID_INP || memory_type == VTA_MEM_ID_WGT)) {
load_queue.write(insn);
} else {
gemm_queue.write(insn);
}
}
}
关于HLS编码的一些观察:
- · 参数:每个功能的参数列表,与接口实用程序结合,定义了所生成的硬件模块公开的硬件接口。
- 通过值传递的参数表示主机可以写入的只读硬件内存映射寄存器。例如,该提取功能具有一个insn_count参数,该参数将被合成为主机要写入的存储器映射寄存器,以便设置给定VTA指令序列的长度。
- 指针参数可能意味着两件事之一,具体取决于所使用的接口编译指示。
- 与m_axi接口编译指示一起使用时,将生成AXI主接口以提供对DRAM的DMA访问。
- 与bram接口编译指令一起使用时,将生成BRAM接口,以将读取和/或写入端口公开给FPGA Block-RAM。
- 通过引用传递的HLS流与axis接口编译指示相结合,产生了到模块的FIFO接口。硬件FIFO在模块之间提供了一种有用的同步机制。
- · 编译指示:编译器编译对于定义每个模块的硬件实现是必不可少的。列出了VTA设计中用于将实现要求传达给编译器的几种编译指示。
- HLS INTERFACE:指定综合硬件模块的接口。
- HLS PIPELINE:通过设置启动间隔目标来定义硬件管道性能目标。当目标被设定,它告诉编译器合成的硬件管线应该能够每个周期执行一个II == 1循环迭代。
- HLS DEPENDENCE:指示编译器在给定循环中忽略某些类型的依赖项检查。考虑一个写入和读取相同BRAM结构的循环体,并且需要实现II为1。HLS编译器必须假定最坏的情况,即对先前写入更新了该周期之前地址的地址进行读取:在给定的BRAM时序特性下,这是无法实现的(至少需要2个周期才能看到更新的值)。为了获得II等于1,必须放松依赖检查。启用此优化功能后,它会落在软件堆栈上,以防止写入之后再读取相同的地址。
笔记
该参考为Xilinx 2018.2工具链提供了更深入,更完整的HLS规范。
架构概述
指令集架构
VTA的指令集体系结构(ISA)由4条具有可变执行等待时间的CISC指令组成,其中两条执行微码指令序列以执行计算。
VTA说明如下:
- LOAD指令:将DRAM中的2D张量加载到输入缓冲区,权重缓冲区或寄存器文件中。将微内核加载到微操作缓存中。在加载输入和权重图块时支持动态填充。
- GEMM 指令:在输入张量和权重张量上执行矩阵矩阵乘法的微操作序列,并将结果添加到寄存器文件张量中。
- ALU 指令:对寄存器文件张量数据执行矩阵矩阵ALU操作的微操作序列。
- STORE 指令:将2D张量从输出缓冲区存储到DRAM。
该LOAD指令由取决于存储存储器缓冲器位置目标的负荷和计算模块执行。在GEMM和ALU指令由计算模块的核心GEMM和张量ALU执行。最后,STORE指令仅由存储模块执行。下图描述了每个指令的字段。
VTA ISA会随着VTA的体系结构参数(即GEMM核心形状,数据类型,内存大小等)的修改而改变,因此ISA不能保证所有VTA变体之间的兼容性。这是可以接受的,因为VTA运行时会适应参数更改,并生成针对要生成的加速器版本量身定制的二进制代码。这体现了VTA堆栈采用的协同设计理念,该理念包含了软硬件接口的流动性。
数据流执行
VTA依靠硬件模块之间的依赖性FIFO队列来同步并发任务的执行。给定的硬件模块如何通过使用依赖性FIFO队列和单读取器/单写入器SRAM缓冲区,以数据流方式从其生产者模块和使用者模块同时执行。每个模块通过写后读(RAW)和读后写(WAR)依赖关系队列连接到其使用者和生产者。
上面的伪代码描述了模块如何执行给定指令,该指令基于对其他指令的依赖性而确定。首先,每个指令内的依赖标志都在硬件中解码。如果指令具有传入的RAW依赖关系,则根据从生产者模块接收到RAW依赖关系令牌来确定执行。如果任务具有传入的WAR依赖关系,则根据从使用者模块接收到的WAR依赖关系令牌来确定执行。当任务完成时,检查传出的RAW和WAR依赖性,分别通知使用者和生产者模块。
在这种情况下,依赖标记是无信息的。这是因为每个模块执行的指令不能按设计重新排序,因为以FIFO顺序到达。
管道扩展性
默认的VTA设计由描述一个三阶段load-compute-store任务管道的四个模块组成。遵循数据流硬件组织原则,可以将VTA扩展到更多阶段。例如,可以设想将张量ALU与GEMM核心分开,以最大程度地利用GEMM核心。这将导致load-gemm-activate-store任务流水线紧密地反映TPU设计。增加更多的阶段会产生成本:增加存储空间和额外的逻辑开销,这就是选择默认的3阶段管道的原因。
微架构概述
描述组成VTA设计的模块。模块定义包含在中3rdparty/vta-hw/hardware/xilinx/sources/vta.cc。
提取模块
VTA由线性指令流编程。提取模块是VTA到CPU的入口,通过三个内存映射寄存器进行编程:
- 读写control寄存器启动读取模块,并对其进行读取以检查其是否完成。
- 只写insn_count寄存器设置要执行的指令数。
- 只写insns寄存器设置DRAM中指令流的起始地址。
CPU在VTA运行时准备的物理连续缓冲区中的DRAM中准备指令流。当指令流准备就绪时,CPU将起始物理地址写入insns寄存器,将指令流的长度写入insn_count寄存器,并在寄存器中声明起始信号control。此过程将启动VTA,VTA将通过DMA从DRAM读取指令流。
访问指令流后,获取模块会部分解码指令,将这些指令推入命令队列,这些命令队列会馈入装入,计算和存储模块:
- STORE 指令被推送到存储命令队列以由存储模块处理。
- GEMM和ALU指令被推到由计算模块被处理的计算命令队列。
- LOAD 描述微操作内核的加载操作或寄存器文件数据的指令被推到计算命令队列中,由计算模块处理。
- LOAD 描述输入或重量数据的加载操作的指令被推到加载命令队列中,以由加载模块进行处理。
当命令队列之一变满时,访存模块将暂停,直到队列未满为止。命令队列的大小应足够深以允许宽阔的执行窗口,并允许多个任务同时在load-compute-store管道中进行。
计算模块
VTA的计算模块充当RISC处理器,该处理器在张量寄存器而非标量寄存器上执行计算。两个功能单元使寄存器文件发生变化:张量ALU和GEMM内核。
计算模块从微操作缓存执行RISC微操作。有两种类型的计算微操作:ALU和GEMM操作。为了最大程度地减少微操作内核的占用空间,同时避免了对诸如条件跳转之类的控制流指令的需求,计算模块在两级嵌套循环内执行微操作序列,嵌套循环通过以下操作计算每个张量寄存器的位置:仿射功能。这种压缩方法有助于减少微内核指令的占用空间,适用于矩阵乘法和2D卷积,这在神经网络运算符中很常见。
所述GEMM芯求值GEMM指令,通过在上述图中所描述的2级嵌套循环执行的微码序列。GEMM内核每个周期可以执行一个输入权重矩阵乘法。单周期矩阵乘法的维数定义了硬件张量内在函数TVM编译器必须将其降低到较低的计算调度表上。张量固有值由输入张量,权重和累加器张量的尺寸定义。每种数据类型都可以具有不同的整数精度:权重和输入类型通常都是低精度的(8位或更少),而累加器张量具有更宽的类型以防止溢出(32位)。为了使GEMM核心保持繁忙,每个输入缓冲区,权重缓冲区和寄存器文件都必须公开足够的读/写带宽。
张量ALU支持一组标准操作来实现共同活化,归一化,池运算符。VTA是模块化设计,可以扩展Tensor ALU支持的算子范围,以提高算子覆盖范围,但要以提高资源利用率为代价。Tensor ALU可以对立即数执行张量-张量运算以及张量-标量运算。张量ALU的操作码和立即数由高级CISC指令指定。张量ALU计算上下文中的微代码仅负责指定数据访问模式。
就计算吞吐量而言,Tensor ALU并非以每个周期一次操作的速度执行。限制来自缺乏读取端口:由于每个周期可以读取一个寄存器文件张量,张量ALU的启动间隔至少为2(即,每2个周期最多执行1次操作)。一次执行单个张量-张量操作可能会很昂贵,特别是考虑到寄存器文件类型很宽(通常为32位整数)。为了平衡Tensor ALU与GEMM内核的资源利用,在多个周期内通过矢量向量操作执行张量-张量操作。
加载和存储模块
加载和存储模块以从DRAM到SRAM的跨越式访问模式执行2D DMA加载。加载模块可以动态插入2D填充,这在阻止2D卷积时很有用。这意味着VTA可以平铺2D卷积输入,无需支付将数据重新布置在DRAM中以在输入和权重平铺块周围插入空间填充的开销。