Data-Level Parallelism¶
约 1506 个字 89 行代码 预计阅读时间 9 分钟
DLP 指的是数据流中存在大量的并行性
SIMD 指令架构是实现数据级并行的主要方法,常用在面向矩阵的科学计算、面向媒体的图像/视频处理等。
相比于 TLP 的 MIMD,SIMD 更高效节能,因为 SIMD 只需要一条指令流控制多个数据流,因此在移动端应用广泛。
Vecter Processor¶
Vector 对标我们之前所说的 Scalar(标量),它使用单独一条指令对一串类数组的向量数据进行操作,节省了 Instruction Fetch 和 Decode 的时间。
注意向量加法也不是同时并行计算的,也是单个单个计算,所以说节省了 IF 和 ID 的时间
我们以一个 32 elements 的向量计算 \(a*\overrightarrow{X} + \overrightarrow{Y}\) 为例:
一个向量中的计算都不应该依赖于前面的结果,这个数据依赖是由编译器来保证的,硬件无需检查,从而确保较高的 Clock Rate。
向量处理器有大量的存储访问需求,其存储器经过特殊设计高度交错,并且不使用 Data Cache。其对内存的带宽具有较高要求。
向量处理器有两种类型:
- Memory-Memory Processor
- 直接操作内存中的数据
- e.g. CDC Star-100, TI ASC
- 内存带宽要求较高
- 检查 Memory 的依赖较为复杂
- Vector Register Processor
- 先将数据加载到寄存器中再进行操作
- e.g. Cray-1, 以及自 1980s 后的所有 Vector Machines
- 我们所说的 Vector Processor 都默认为 Vector Register 类型(包括后面讨论的)
以下程序用于简单对比二者区别:
在没有结构冲突和数据冲突的条件下,一个向量计算的执行时间取决于向量长度。RV64V 指令集中的 FU 一个周期可以处理一个向量元素,因此其执行时间可认定为向量的长度。
每个机器有自己的最大向量长度 MVL,即一条指令最多能处理 MVL 个元素。如果向量长度 VL>MVL,则需要对其拆分,生成 VL/MVL 个长度为 MVL 的向量循环。这一操作叫做 Strip Mining。
有 \(VL=N\), \(MVL<N\)
- 先处理 \(N\mod MVL\) 个元素
- 然后分批处理 \(MVL\) 个元素,共需 \(\lfloor N / MVL \rfloor\) 组
Additional Reading: Dynamic Register Type
动态寄存器类型指寄存器本身的有效向量长度 VL(Vector Length) 在执行时可以动态设置,而不是在编译时写死。VL 存储在 Vector-Length Register 中,可以通过指令 setvl
设置。动态寄存器类型特点为:
- 对硬件透明,允许写出与硬件无关的向量代码
- 灵活性高,适应不同向量宽度的硬件,并且支持隐式类型转换
- 如果数据长度大于 MVL,则自动分批执行(strip mining)
另外,在向量寄存器总空间不变的前提下,我们可以选择性的禁用几个向量寄存器,这样每个向量寄存器分配到位数得以增加,从而扩大 MVL 的值。向量寄存器中存储的数据类型也可以由用户指定。
Vector Chaining
相当于向量版本的 bypassing/forwarding
对于有前后依赖的指令,如果有链接技术,则前一个指令的一个分量完成则可以直接进入下一个指令,并不用等待全部分量计算完成。
- Convey: 一组可以一起执行的指令
- 要求不包含结构冲突,但是因为 chain 的存在允许 RAW 冲突
- Chimes: 通过链接技术产生的执行序列,是流水线的调度时间单位
- 通常情况下,一个 Convey 对应一个 Chime,一个 Chime 需要 \(VL\) 个时钟周期
- Chaining: 链接技术
- FLOP: 浮点运算,,例如一个对 32 element vector 计算的
vmul
,其 FLOP = 32
这里最后的 vst
为什么不能和前面两条指令链在一起呢?
我们需要保证存储数据的正确性,所以存储指令 vst
必须等到整个向量均有效时才能开始写回内存。
这里的 cycles per FLOP 的计算流程实际如下
Total Cycles = 3 * 32; Total FLOP = 2 * 32; Cycles per FLOP = 3 / 2;
Question
Conditional Execution
我们考虑额外设置一个 Predicate Register(Masked Vector) p0
,通过掩码来控制向量的执行。例如掩码 1010,则只有第 0 个和第 2 个元素会执行运算。
这样,我们在执行时先扫描一遍掩码,只执行需要的元素。而传输数据时也只传输需要的元素,从而节省带宽。
Gather/Scatter on Sparse Matrix
对于稀疏矩阵,我们访问或存入可以使用 Gather/Scatter 操作:
- Gather 运算接受一个索引向量,并通过与基地址相加来组成一个新向量,结果为储存在一个向量寄存器中的稠密向量
- 在完成对稠密向量的聚集操作后,以 Scatter 的形式进行存储
在 RV64V 中,我们通过 vldx
和 vstx
来通过索引进行 Load/Store 操作。
Multi-Lane Implementation
划分为多个 Lane,同一个向量的不同分量可以在不同的 Lane 上存储和计算,通道的增加能够提升向量单元的吞吐量峰值:
GPU¶
GPU(Graphics Processing Unit) 按线程执行,属于 Single Instruction Multiple Threads。
Single Instruction Multiple Threads 支持 MIMD、SIMD、ILP 等
一组线程称为 Block,Block 又被排列成 Grid。
GPU 与向量机不同的是:
- 没有标量处理器
- 通过多线程来减小内存延迟
- 有非常多 FU
CUDA 全称为 Compute Unified Device Architecture。
Loop-Level Parallelism¶
基于循环的并行主要看循环的迭代之间是否有数据依赖(loop-carried dependence) ,即后续迭代的数据访问是否依赖于先前迭代的输出,例如:
循环间依赖可以通过改写解决:
改写后不仅解决了循环间依赖,还可以使用向量链接
另一个例子尝试对如下递归依赖进行改写:
注意到对于标量 sum
,它依赖于上一次循环结束时自己的值,因此我们并不能简单将这个循环并行。此时我们可以使用标量扩展(scalar expansion)技术来进行转换,使其可以完全并行:
其中代码的上半部分可以完全并行执行,而代码的下半部分可以使用专用硬件进行对归约操作的加速: