1. 简介
VPU向量计算单元(Vector Processing Unit)是专门设计用于处理向量数据和执行向量操作的计算硬件单元。它是现代计算机体系结构中一种重要的组件,特别是在处理需要高效并行计算的任务时,VPU能够显著提高性能。
VPU是RISC-V的vector标准指令集的实现,允许处理器同时处理多个数据元素,提高计算效率,可用于各种算子的计算。
VPU微架构
2. 向量计算
给定如下场景:输入两个uint32类型,长度为256的向量va和vb,输出结果vc。
通过CPU执行计算,代码及汇编如下:
1. 从地址va和vb,根据偏移i,读取1个uint32数据到寄存器中;
2. 执行加法指令;
3. 将结果从寄存器存储到1个uint32数据到地址vc[i]中;
4. i+1,判断是否到256,小于256则重复步骤1~3。
通过VPU执行计算,代码及汇编如下:
1. 配置VPU参数;
2. 从地址va和vb中,各读取256个数据到寄存器中;
3. 执行加法指令;
4. 将结果从寄存器,存储256个数据到地址vc中。
区别:
使用CPU执行向量计算,需要循环256次,每个循环进行2次load,1次store,1次加法,1次i+1,1次条件跳转;
使用VPU执行向量计算,只需要执行2次load,1次加法,1次store。
在上面的场景中,使用VPU时,可以一次load 256个uint32数据,并行计算256个加法,再一次store回256个uint32数据。可以显著提高计算的效率。
3. 基本原理
VPU有7个CSR和32个vector regs:
CSR regs:
vstart:向量指令要执行的第一个element的索引,任意一条向量指令执行时,vstart之前的向量将被忽略,该条向量执行结束后vstart被置0,所有向量指令包括vset{i}vl{i}执行结束后都会把该寄存器置零;
vxsat:定点饱和标志;
vxrm:定点数四舍五入模式;
vcsr:向量控制和状态寄存器;
vl:实际执行的向量长度,只能在一条vsetvset{i}vl{i}指令执行后自动更新vtype:向量数据类型,只能在一条vset{i}vl{i}指令执行后自动更新,而其标识了向量的实际宽度、组织方式、mask处理策略、tail处理策略等多种信息;
vlenb:VLEN/8,以bytes数来计算VLEN,用于某些需要bytes来表示VLEN的场景。
Vector regs:
VLEN:vector寄存器位宽,硬件参数,可设定为1024,即每个寄存器有1024个bits;
SEW:计算时每个元素的位宽,软件参数,可选值为4/8/16/32/64;
LMUL:计算时使用的寄存器组数,软件参数,可选值为:1,2,4,8,1/2,1/4,1/8;
VL:当前指令操作的元素个数。
隐形参数:
VL_MAX:一条指令最多可以操作的元素个数,计算公式为:LMUL*(VLEN/SEW)
以前文VPU程序举例,说明几个参数的作用。
设置每个元素位宽为32,使用8组寄存器,处理a0个元素,tail元素为undisturbed处理,mask元素undisturbed处理。
li a0,256vsetvli t0,a0,e32,m8,tu,muvle32.v v0,%[va] #将va地址的值load到v0寄存器中vle32.v v8,%[vb] #将vb地址的值load到v8寄存器中vadd.vv v0,v0,v8 #将v0和V8寄存器的值进行加法操作vse32.v v0,%[vc] #将v0寄存器store中vc地址中
VL_MAX = LMUL * (VLEN/SEW) = 8 * (1024/32) = 256
对于尾部元素/mask元素的处理策略
tu/ta:tail undisturbed/tail agnostic
mu/ma:mask undisturbed/mask agnostic
undisturbed:值不会被改变;agnostic:可以覆写为全1或者保持不变
4. 指令
指令可分为三类:配置指令,Ioad/store指令,算数指令
4.1 配置指令
配置指令用于配置接下来的vector指令的类型(即配置元素类型,元素个数,寄存器组数等)
4.2 Ioad/store指令
基本向量扩展支持单位跨步(unit-stride),跨步(strided)和索引(indexed)寻址模式。
unit-stride就是向量元素在内存中的排布就是挨个的,可以直接一个一个取出来。
strided是为了按照一个固定间隔取向量元素的方式。比如在进行两个矩阵相乘A*B,A的一行会跟B的一列做向量乘。A和B在内存中都是按行存储的。A做向量乘时,可以挨个拿出来放到向量寄存器,而B需要按列取出,所以需要每隔一行元素个数取一个元素,放到向量寄存器,才能取出B的一列。此时就需要用到stride模式了。
indexed模式是最精细的,可以精确控制向量寄存器中的某个元素从哪里来。在取每个元素时,用vs2向量寄存器的值在mem中索引要取出的元素。数据的读取还可以区分为有序和无序。对于需要强访存顺序依赖的区域,例如IO,需要使用ordered indexed方式才能保证顺序。

4.3 算数指令
可以分为几类:整数指令,定点指令,规约指令,mask指令,置换指令,浮点指令
5. Intrinsic
除了直接手写汇编以外,riscv官方提供了一套封装好的intrinsic接口,直接调用对应的指令。
以vadd为例,该条算数指令有如下一些参数:
寄存器类型:使用两个vector寄存器相加(vv),使用一个vector寄存器和一个通用寄存器相加(vx),使用一个vector和立即数相加(vi)
元素类型:8,16,32,64
寄存器组数:1,2,4,8,1/2,1/4,1/8
操作元素个数:1~VLMAX
mask:是否有元素mask
对于一条vadd指令,官方提供了如下intrinsic接口:
6. 验证笔记
对于VPU验证,采用开源工程riscv-vector-test进行验证。
该工程可以生成随机的数据,对指令不同的参数进行遍历,通过官方模拟器spike跑出一个结果。
将同样的程序在VPU跑,比对实际跑出来的结果和spike跑出的结果是否一致,来确认功能是否正常。
配置指令
遍历配置指令(vsetvli/vsetivli/vsetvl)的不同类型配置,检查vstart,vtype,vl等是否符合预期。
load/store指令
遍历不同load/store的模式(unit-stride,strided,indexed等),不同元素宽度(e8,e16,e32,e64),不同组数(LMUL=1,2,4,8),向量数量(0~VLMAX),是否mask,对比load/store的值与预期是否相符。
算数指令
遍历不同算数指令(整形,定点,浮点,规约,置换,mask),不同元素宽度(e8,e16,e32,e64),不同组数(LMUL=1,2,4,8),向量数量(0~VLMAX),是否mask,对比计算的值与预期是否相符。
7. 应用
即使有intrinsic指令,直接手写指令来做运算,还是有不小难度。
7.1 计算库
对于上位机而言,可以调用已经移植好riscv vector指令集的算术库,直接使用vector指令进行计算。
BLAS是一个数学计算库的标准,定义了一套矩阵数组操作的API,例如:sgemm float矩阵乘法、sgemv float矩阵乘以数组...诸如此类。
BLAS(Basic Linear Algebra Subprograms 基础线性代数程序集)程序集最初发布于1979年,并用于建立更大的数值程序包(如LAPACK)。
在高性能计算领域,BLAS被广泛使用。例如,LINPACK的运算成绩则很大程度上取决于BLAS中子程序DGEMM的表现。为提高性能,各软硬件厂商则针对其产品对BLAS接口实现进行高度优化。
OpenBLAS是基于GotoBLAS21.13BSD版本进行研发并开源。
7.2 自动向量化
目前LLVM框架下支持的自动向量化工具有SLPVectorize和LoopVectorize。
SLPVectorize
SLP向量化关注单次迭代间的向量化机会,在基本块中搜集相似的标量指令,通过将相似标量指令合并为向量指令的方式,来实现向量化。如下程序可以通过构造向量(a1,a2)和(b1,b2),完成向量化算术运算。
LoopVectorize
循环向量化关注循环迭代间的向量化机会,将多次迭代处理的数据利用更宽的位宽寄存器存储,使其一次能够完成多次循环迭代的数据处理,此后每次循环迭代的下标步长将扩宽成SIMD的位宽/向量元素的位宽。
8. 参考资料
《riscv-v-spec-1.0.pdf》
《v-intrinsic-spec.pdf》

