# 环境配置
# 一、Gemmini 环境配置
# 1. 配置 Chipyard 环境:
根据安装提示,必须在 linux 中进行。第一次没注意,使用 windows 结果卡在 “Collecting package metadata”。
由于 conda 和 git 之前已经安装过,现在只要安装 conda-lock。执行 “./build-setup.sh riscv-tools” 指令后会有漫长的等待。
中途虚拟机磁盘容量不足了,不得不再来一次。直接在 VMware 设置扩容是不够的,还要在虚拟机中设置文件分区大小。 建议磁盘空间 50G 以上!
报错:./build-setup.sh riscv-tools 运行时报错重叠的输出路径
重新安装无效,改 chipyard 版本无效,若暂且搁置后面能运行到第 4 步。
后来尝试在根目录下安装,结果出现一大堆警告,最后在 common.mk 的 380 行强行终止。
此时再继续会导致 “make -C software/libgemmini install” 指令不通过。
解决方法:多运行几次 init-submodules-no-riscv-tools.sh 和 build-toolchain-extra.sh 两个脚本后再次尝试,发现正常了。
其中发现有因为网络问题无法连接的情况,科学 上网也没用。
不过好像不影响。
# 2.Setting Up Gemmini
这一步很顺利,按流程即可。
# 3.Building Gemmini Software
按照流程执行到 “./build.sh” 时,报错 “autoconf: 未找到命令”
解决方法:安装缺少的依赖,输入 “sudo apt install -y autoconf”
# 4.Building Gemmini Hardware and Cycle-Accurate Simulators
报错:环境变量 PATH 找不到 verilator
解决方法:sudo apt-get install verilator
update:改 conda 环境
报错:找不到 libriscv
原因:可能是之前报错的指令 “./build-setup.sh riscv-tools” 导致,缺少某些包。重新加载 env.sh 无效
解决方法:发现并不是上述原因,是没有改 conda 环境,应该改成如下所示,之后的操作必须改 conda!
# 5.Building Gemmini Functional Simulators
报错:make 错误
会影响 6 (2) 操作,若 6 (2) 报错找不到上图文件,4、5 可以多执行几次。注意 conda 环境。
# 6.Run Simulators
# (1)Run a large DNN workload in the functional simulator
运行结果:
# (2)Run a smaller workload in baremetal mode, on a cycle-accurate simulator
报错:找不到文件
解决方法:重复 4、5 中的操作,多 make 几次,会出现下面这个文件
运行结果:
# (3)Run a smaller workload with the proxy-kernel, on a cycle accurate simulator
运行结果:
# 二、buddy-mlir 环境配置
# 1.LLVM/MLIR Dependencies
安装 llvm 时报错:cmake 失败
想办法重新装 llvm,结果又报错:用不了 Ninja
解决方法:降低 cmake 版本无效,安装 ninja-build 后成功
报错:内存不足,build 被终止
解决方法:限制进程数, ninja -C build check-llvm -j4
限制 4 进程
链接报错:
解决方法:可能是子任务出错,重新跑一遍就行。
报错: ctime:80:11: error: 'timespec_get' has not been declared in '::'
解决方法:论坛上说是 conda 环境和系统环境冲突,尝试用 conda upgrade -c conda-forge --all
更新 conda 无效。搞到后来又发现环境不一致问题,服了。考虑到一开始用的是 miniconda3,最后改为安装 miniforge3 就不报错了。
# 2.Clone and Initialize
很顺利
# 3.Build and Test LLVM/MLIR/CLANG
等待时间较长,还是一步安装的那个比较好 hh
其中运行 ninja check-mlir check-clang
时有一个没 pass
解决方法还没找到。它是一个无关紧要的包,不影响后续流程。
报错:CMakeLists.txt 不匹配
解决方法:这是由于重复 cmake 且两次缓存不符导致的,删除 CMakeCache.txt 文件即可。
# 4.Build buddy-mlir
后面的都能正常运行,最后一条测试 ninja check-buddy 结果:
# 三、buddy-benchmark 环境配置
# 1.Choose and Build Dependencies
很顺利
# 2.Gemmini Benchmark
报错:找不到 buddy-mlir 的包
解决方法:添加一下环境变量
报错:找不到路径
解决方法:上一步中的目录改成自己 build 的地址
报错:找不到 riscv64-unknown-linux-gnu-g++
解决方法:换成 chipyard 中的 conda 环境就行
报错:找不到头文件
解决方法:尝试从另一个文件中获取同名文件看看能不能用,能通过但是最后有一个其它地方报错了,说明不仅仅是缺少这一个头文件的问题,应该是之前的安装有些疏忽。换了 miniforge 环境,重复一遍 buddy-mlir 的安装,再走一遍,头文件就能找到了。
头文件解决后,ninja 时又出现如下报错:
解决方法:尝试过重装 chipyard、buddy-mlir、自行配置 llvm 均无效,最后在 issue 中找到方案。需要安装 git lfs,保证 ResNet-101 被成功 clone,具体代码如下:
curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash | |
sudo apt-get install git-lfs | |
git lfs install |
重新 clone 一下 buddy-benchmark,发现这次可以运行了
果然是缺少文件。
最后,修改一下 chipyard 中 GemminiCustomConfigs.scala 文件配置,将 baselineInferenceConfig 改为 defaultFpConfig,重新生成 spike。
完结撒花~~~
# Gemmini 架构
# 一、Gemmini 概述
Gemmini 项目是一个全系统、全栈的 DNN 硬件探索和评估平台,使得我们能够深入了解系统和软件堆栈之间的相互作用对 DNN 性能的影响。结构图如下所示:
Gemmini 使用 RISC-V 自定义指令实现 RoCC 加速器,其单元使用 Rocket 或 BOOM 块的 RoCC 端口,默认情况下通过系统总线连接到内存系统。
该加速器的核心是脉动阵列,主要用于执行矩阵乘法,支持输出平稳和权重平稳数据流。
阵列的输入和输出存储在由成组 SRAM 组成的显式管理暂存器中,由 DMA 加速暂存器数据和主存数据的传输。
此外,由于在处理权重平稳数据流时需要使用脉动阵列外部的累加器,所以需要添加一个配备加法器单元的最终 SRAM 组,这样脉动数组就能将结果存入任意地址的累加器或者从累加器读取数据。
# 二、脉动阵列
脉动阵列是加速器的核心组成部分,结构如下:
其主要参数有:
- 脉动数组维度 (tileRows, tileColumns, meshRows, meshColumns):脉动数组由 2 级层次结构组成,其中每个图块都是完全组合的,而图块网格在每个图块之间具有管道寄存器。
- 暂存器和累加器存储器参数 (sp_banks、sp_capacity、acc_capacity):暂存器或累加器的总容量(单位 KB)
- 数据流参数 (dateflow):确定 Gemmini 中的脉动数组是输出平稳还是重量平稳。
- 类型参数 (inputType、outputType、accType):确定流经 Gemmini 加速器不同部分的数据类型,例如 8 位定点数、32 位整数等。
- 访问执行队列参数(ld_queue_length、st_queue_length、ex_queue_length、rob_entries):为实现访问执行解耦,Gemmini 加速器具有加载指令队列、存储指令队列和执行指令队列。这些队列的相对大小决定了访问执行解耦的级别。
- DMA 参数 (dma_maxbytes、dma_buswidth、mem_pipeline):Gemmini 实现 DMA 将数据从主存储器移动到 Gemmini 暂存器,以及从 Gemmini 累加器移动到主存储器。这些 DMA 事务的大小由 DMA 参数决定。这些 DMA 参数与 Rocket Chip SoC 系统参数紧密耦合。
脉动阵列中最基础的单元为 PE,功能是实现一维乘加运算。大量 PE 排列成方形,组成一个 Tile,Tile 之间彼此相连,两两之间有寄存器可以利用。计算前需要将数据存入脉动阵列或者周围(图中红线和蓝线所连接)的存储器中。
# 三、软件部分
Gemmini 在配置指令、数据移动指令和矩阵乘法执行指令方面指定指令集。然后生成器将 Gemmini 自定义指令包装到 DNN 运算符中,相关宏定义可见 software/gemmini-rocc-tests/include/gemmini.h
。生成器还会根据参数生成 C 头文件,共同编译,调整库性能。此外,还可以通过 Microsoft ONNX-Runtime 框架的端口运行 ONNX 指定的神经网络。
# 四、内存寻址
下图是 2*2 脉动阵列内存寻址的示意图。内存寻址按行进行,图中 Tile 一行有 2 个 PE 单元,则宽度 DIM 为 2。其中暂存器数据类型是 8 位的 int 值,累加器数据类型是 32 位的 int 值。
Gemmini 中的内存编码均为 32 位长,最高的 3 位是保留位,用于区分寻址累加器还是暂存器、写入时是覆盖还是继续累加、从何处读取数据、是否读取按比例缩小的数据等等。
# 五、指令
这里只列举两个数据移动指令,更多的指令可以在 Gemmini 仓库查看。
# (i)将数据从主存储器移至暂存器:mvin
指令 mvin rs1, rs2
中,rs1 是加载到暂存器的虚拟 DRAM 地址,rs2 的 031 位记录本地暂存器或累加器地址,3263 位记录要加载的行列数。即数据从 rs1 流向 rs2。其中加载的列数必须小于 Tile 中每行 TE 宽度 DIM,否则会拆分后分块加载。
# (ii)将数据从暂存器移至 L2/DRAM:mvout
指令 mvout rs1, rs2
中,rs1 是从暂存器写入的虚拟 DRAM 地址,rs2 的 031 位记录本地暂存器地址,3263 位同样记录加载行列数。即数据从 rs2 流向 rs1。
# MLIR
# 一、MLIR 概述
IR 是深度学习中模型的中间表示,包括算子和数据。但由于当前 IR 众多,造成维护和迁移的困难,而 MLIR 的出现就是为解决这个问题。
首先,MLIR 提出了 Dialect,即各种 IR 需要学习的语言,一旦某种 IR 学会这种语言,就可以基于这种语言将其重写为 MLIR。
此外,MLIR 还通过 Dialect 抽象出了多种不同级别的 MLIR,以应对 IR 跨度大的难题。例如源程序在经过语法树分析和 MLIR 分析后,得到的目标程序再作为下一次编译的源程序,通过 MLIR Dialect 多次下降,最终得到可执行的机器码,类似于渐进式学习。
# 二、toy 语言
MLIR 中提供了 toy 语言以说明 MLIR 的执行流程,是验证和说明说用,因此语法和功能都非常简单。它是一种基于张量的语言,只支持 64 位浮点类型数据,适合于进行函数定义和数学计算。具体语法见官网。
按照官网的语法教程看如下一个 MLIR 编译流程的例子,主要进行矩阵转置相乘。
# User defined generic function that operates on unknown shaped arguments.
def multiply_transpose(a, b) {
return transpose(a) * transpose(b);
}
def main() {
# Define a variable `a` with shape <2, 3>, initialized with the literal value.
var a = [[1, 2, 3], [4, 5, 6]];
var b<2, 3> = [1, 2, 3, 4, 5, 6];
# This call will specialize `multiply_transpose` with <2, 3> for both
# arguments and deduce a return type of <3, 2> in initialization of `c`.
var c = multiply_transpose(a, b);
# A second call to `multiply_transpose` with <2, 3> for both arguments will
# reuse the previously specialized and inferred version and return <3, 2>.
var d = multiply_transpose(b, a);
# A new call with <3, 2> (instead of <2, 3>) for both dimensions will
# trigger another specialization of `multiply_transpose`.
var e = multiply_transpose(b, c);
# Finally, calling into `multiply_transpose` with incompatible shape will
# trigger a shape inference error.
var f = multiply_transpose(transpose(a), c);
}
该程序生成 AST 再生成初级 MLIR 如下:
module {
func @multiply_transpose(%arg0: tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":4:1), %arg1: tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":4:1)) -> tensor<*xf64> {
%0 = toy.transpose(%arg0 : tensor<*xf64>) to tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":5:10)
%1 = toy.transpose(%arg1 : tensor<*xf64>) to tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":5:25)
%2 = toy.mul %0, %1 : tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":5:25)
toy.return %2 : tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":5:3)
} loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":4:1)
func @main() {
%0 = toy.constant dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":9:17)
%1 = toy.reshape(%0 : tensor<2x3xf64>) to tensor<2x3xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":9:3)
%2 = toy.constant dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":10:17)
%3 = toy.reshape(%2 : tensor<6xf64>) to tensor<2x3xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":10:3)
%4 = toy.generic_call @multiply_transpose(%1, %3) : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":11:11)
%5 = toy.generic_call @multiply_transpose(%3, %1) : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":12:11)
toy.print %5 : tensor<*xf64> loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":13:3)
toy.return loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":8:1)
} loc("../../mlir/test/Examples/Toy/Ch2/codegen.toy":8:1)
} loc(unknown)
代码中红色部分是冗余的 reshape 操作,需要通过 Pass 来识别和优化冗余。此时有两种途径,一种是使用 C++ 编写匹配和重写函数来消除冗余,另一种是采用 DRR 自动生成函数。
优化后 main 函数部分表达式如下,可见已经非常简化了。
module {
func @main() {
%0 = toy.constant dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>
%1 = toy.transpose(%0 : tensor<2x3xf64>) to tensor<3x2xf64>
%2 = toy.mul %1, %1 : tensor<3x2xf64>
toy.print %2 : tensor<3x2xf64>
toy.return
}
}
该段代码还是比较高层的,还可以继续部分 Lowering,混合不同的 Dialect Lowering 到 LLVM IR。
总体流程可以如下表示:
# 三、Gemmini 方言分析
由于指令较多,这里只选取几个详细讲解。
# (1)mvin-mvout
mvin-mvout-run:
@${BUDDY_OPT} ./mvin-mvout.mlir -lower-gemmini | \
${BUDDY_TRANSLATE} --buddy-to-llvmir | \
${BUDDY_LLC} -filetype=obj -mtriple=riscv64 \
-mattr=+buddyext,+D -float-abi=hard \
-o log.o
@riscv64-unknown-linux-gnu-gcc log.o -O2 -static -o a.out
@spike --extension=gemmini pk a.out
在上述代码的第 2 行,操作是执行了 mvin-mvout.mlir,然后有一个 lower 操作,下降到 llvmir。比较简单的例子都是这样的结构,只是前面操作的 mlir 文件有所不同。在此打开 mvin-mvout.mlir 文件。
// RUN: buddy-opt %s \
// RUN: --lower-gemmini | \
// RUN: FileCheck %s
memref.global "private" @gv : memref<2x16xi8> = dense<[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]]>
func.func @main() -> i64 {
%0 = arith.constant 0 : i64
%stride16 = arith.constant 16 : i64
%stride8 = arith.constant 8 : i64
%spadAddr = arith.constant 0 : i64 //暂存器地址
%arrayA = memref.get_global @gv : memref<2x16xi8> //获取全局变量
%arrayB = memref.alloc() : memref<3x16xi8> //分配内存
%arrayC = memref.alloc() : memref<2x8xi8>
gemmini.print %arrayB : memref<3x16xi8>
gemmini.print %arrayC : memref<2x8xi8>
// CHECK: "gemmini.intr.config_st"
// The mvout op's stride is 16.
gemmini.config_st %stride16 : i64 //确定mvout的步长16
// CHECK: "gemmini.intr.config_ld"
// The mvin op's stride is 16
gemmini.config_ld %stride16 : i64 //确定mvin的步长16
// CHECK: "gemmini.intr.mvin"
gemmini.mvin %arrayA %spadAddr : memref<2x16xi8> i64
// CHECK: "gemmini.intr.mvout"
gemmini.mvout %arrayB %spadAddr : memref<3x16xi8> i64
// CHECK: "gemmini.intr.config_st"
// The mvout op's stride is 8
gemmini.config_st %stride8 : i64 //步长为8,因为C矩阵大小为2*8,按行寻址,DIM为每行的PE数
// CHECK: "gemmini.intr.mvout"
gemmini.mvout %arrayC %spadAddr : memref<2x8xi8> i64
gemmini.print %arrayB : memref<3x16xi8>
gemmini.print %arrayC : memref<2x8xi8>
return %0 : i64
}
从上往下,第 5 行是全局变量的声明,然后 main 函数中有三个矩阵,分别是 A、B、C,其中 A 赋值为全局变量,B、C 仅分配了内存。然后在第 20 行有一条 config_st 指令,需要查阅 td 文件。在 td 文件中找到 config_st 的定义:
def ConfigStOp : Gemmini_Op<"config_st"> {
let summary = "Config store operation";
let description = [{
TODO: consider the attribute type according to Gemmini spec.
}];
let arguments = (ins I64:$stride,
DefaultValuedAttr<I64Attr, "0">:$activation,
DefaultValuedAttr<F32Attr, "1.0">:$scale);
let assemblyFormat = "$stride attr-dict `:` type($stride)";
}
从 description 中可以看出,这条指令用于根据特定的 Gemmini 确定 mvin 步长参数。
第 23 行的 config_ld 同理也是确定步长的,只不过是确定 mvout 的步长而已。
接下来就是矩阵 A 数据移动到暂存器,暂存器数据移动到矩阵 B 的过程。当暂存器中的数据移动到矩阵 C 时,需要指定步长为 8,因为 C 矩阵的大小为 2*8,根据之前所说的按行寻址模式,宽度为 8。当然,宽度不能超过脉动矩阵的最大宽度,即一个 Tile 中一行的 PE 元件数量,否则会发生切割后分块传输。
最后是一些打印过程,不再赘述。
# (2)matmul-os
matmul-os-run:
@${BUDDY_OPT} ./matmul-os.mlir -lower-gemmini | \
${BUDDY_TRANSLATE} --buddy-to-llvmir | \
${BUDDY_LLC} -filetype=obj -mtriple=riscv64 \
-mattr=+buddyext,+D -float-abi=hard \
-o log.o
@riscv64-unknown-linux-gnu-gcc log.o -O2 -static -o a.out
@spike --extension=gemmini pk a.out
这是一个矩阵相乘,并且输出稳定的例子。同样地,打开 matmul-os.mlir 文件如下:
// RUN: buddy-opt %s \
// RUN: --lower-gemmini | \
// RUN: FileCheck %s
memref.global "private" @gv1 : memref<4x4xi8> = dense<[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]>
memref.global "private" @gv2 : memref<4x4xi8> = dense<[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]]>
func.func @main() -> i64 {
%in = memref.get_global @gv1 : memref<4x4xi8> //获取全局变量
%identity = memref.get_global @gv2 : memref<4x4xi8> //获取全局变量
%out = memref.alloc() : memref<4x4xi8> //分配内存
gemmini.print %out : memref<4x4xi8>
%inSpAddr = arith.constant 0 : i64 //指定输入暂存器的地址
%outSpAddr = arith.constant 4 : i64 //指定输出暂存器的地址
%identitySpAddr = arith.constant 8 : i64
%cst4 = arith.constant 4 : i64
%cst0 = arith.constant 0 : i64
// CHECK: "gemmini.intr.config_st"
gemmini.config_st %cst4 : i64 //指定输入暂存器的步长
// CHECK: "gemmini.intr.config_ld"
gemmini.config_ld %cst4 : i64 //指定输出暂存器的步长
// CHECK: "gemmini.intr.mvin"
gemmini.mvin %in %inSpAddr : memref<4x4xi8> i64
// CHECK: "gemmini.intr.config_ld"
gemmini.config_ld %cst4 : i64
// CHECK: "gemmini.intr.mvin"
gemmini.mvin %identity %identitySpAddr : memref<4x4xi8> i64
// CHECK: "gemmini.intr.config_ex"
gemmini.config_ex {dataflow = 0 } //配置执行管道
// CHECK: "gemmini.intr.preload"
gemmini.preload_zeros %outSpAddr %cst4 %cst4 : i64 i64 i64 //预加载为0
// CHECK: "gemmini.intr.compute_preloaded"
//执行矩阵乘法
gemmini.compute_preloaded %inSpAddr %identitySpAddr %cst4 %cst4 %cst4 %cst4 : i64 i64 i64 i64 i64 i64
// CHECK: "gemmini.intr.mvout"
gemmini.mvout %out %outSpAddr : memref<4x4xi8> i64 //输出到主存
gemmini.print %out : memref<4x4xi8>
return %cst0 : i64
}
其中,config_ex 指令用于配置执行管道,包括配置数据流是输出稳定还是权重稳定,是否转置等等。值得一提的是,转置操作也是直接通过 config_ex 指令实现的。preload_zeros 指令用于在输出的暂存器地址上预置 0,因为默认是会将计算结果直接加上当前数。compute_preloaded 指令则是用于计算矩阵相乘的结果,最后从 outAddr 取出计算结果。