ECE408 Lecture 17 Parallel Scan Part 2
这次回顾ECE408 Lecture 17,这次介绍了Parallel Scan第二部分。
课程主页:
搬运视频:
目标
- 掌握并行扫描(前缀和)算法
- 工作效率与延迟;
- Brent-Kung树算法;
- 分层算法;
Kogge-Stone并行Scan算法
回顾
回顾Kogge-Stone并行Scan算法:
提高效率
- 一种常见的并行算法模式:
- 平衡树;
- 在输入数据上构建一个平衡二叉树,并将其从root开始遍历;
- 树不是一种实际的数据结构,而是一种概念模式;
- 对于scan:
- 从叶子节点向上遍历到根,在树的内部节点处构建部分和;
- root是所有叶子节点的总和;
- 从叶子节点向上遍历到根,在树的内部节点处构建部分和;
Brent-Kung并行scan算法
Brent-Kung并行scan步骤
第一步先计算一些特殊的部分和,称为Scan Step:
第二步,根据第一步的结果再次求和,称为Post-Scan Step:
数据流:
Scan Step Kernel
// float T[2*BLOCK_SIZE] is in shared memory
// for previous slide, BLOCK_SIZE is 8
int stride = 1;
while(stride < 2*BLOCK_SIZE) {
__syncthreads();
// 2n - 1
int index = (threadIdx.x+1)*stride*2 - 1;
if(index < 2*BLOCK_SIZE && (index-stride) >= 0)
T[index] += T[index-stride];
stride = stride*2;
}
在之前的例子中:
// threadIdx.x+1 = 1, 2, 3, 4, 5, 6, 7, 8
// stride = 1, index = 1, 3, 5, 7, 9, 11, 13, 15
分析:
- 在Scan Step中,索引为$i=2^k(2s + 1) - 1$的位置的和为$\sum_{j=i-2^{k} +1}^i a_j$;
- 在Post Step中,每一步是使得上面求和的下界$j$逐渐减少到$0$;
- 这是直观理解,完全理解需要仔细分析;
Post Scan Step Kernel
int stride = BLOCK_SIZE/2;
while(stride > 0) {
__syncthreads();
int index = (threadIdx.x+1)*stride*2 - 1;
if ((index+stride) < 2*BLOCK_SIZE)
T[index+stride] += T[index];
stride = stride / 2;
}
// In our example,
// BLOCK_SIZE=8 stride=4, 2, 1
// for first iteration, active thread = 0 index = 7, +stride = 11
分析
- 并行scan执行$2\log(n)$次并行迭代;
- Scan和Post Scan分别使用$\log n$;
- 一共有$n/2, n/4,\ldots, 1, (2-1), \ldots, (n/4-1), (n/2-1)$次加法;
- 在我们的示例中,$n = 16$,加法次数为$16/2 + 16/4 + 16/8 + 16/16 + (16/8-1) + (16/4-1) + (16/ 2-1)$;
- 总加法次数:$(n-1) + (n-2)-(\log(n) -1) = 2\times (n-1) – \log(n)=O(n)$;
- 加法的总数不超过高效顺序算法的两倍;
- 在硬件充足的情况下,并行可以轻松克服两倍的工作量;
Kogge-Stone vs. Brent-Kung
- 与Kogge-Stone相比,Brent-Kung使用的线程数只有一半;
- 每个线程应该将两个元素加载到共享内存中;
- Brent-Kung的步数是Kogge-Stone的两倍;
- Kogge-Stone在GPU中使用block进行并行scan更受欢迎;
- 块中的线程被“占用”,直到整个块完成;
完整Scan的整体流程,分层方法
在CUDA中使用全局内存
- 一个线程块的寄存器和共享内存中的数据对其他块不可见;
- 要使数据可见,必须将数据写入全局内存;
- 但是,写入全局内存的任何数据在内存fence之前都是不可见的,这通常是通过终止内核执行来完成的;
- 启动另一个内核以继续执行,由终止的内核完成的全局内存写入对所有线程块都是可见的。
处理任意长度输入的Scan
- 基于Scan内核构建,最多可处理来自Brent-Kung的$2\times \mathrm{blockDim}.x$个元素;
- 对于Kogge-Stone,将$\mathrm{blockDim}.x$元素的每个部分分配给一个块;
- 让每个块使用其$\mathrm{blockIdx}.x$作为索引将其部分和写入Sum数组
; - 对Sum数组运行并行Scan;
- 如果对一个块来说太大,可能需要将Sum分解成多个部分;
- 将扫描的Sum数组值添加到对应的元素部分;
Inclusive Scan定义
定义:
扫描操作采用二元结合运算符$\oplus$和$n$个元素的数组:
并返回前缀和数组:
例子:
如果$\oplus$是加法,那么在数组$[3,1,7,0,4,1,6,3]$上执行exclusive scan操作的结果会返回$[0, 3,4,11,11,15,16,22]$。
为什么要Exclusive Scan
- 使用缓冲区的起始地址;
- Inclusive和Exclusive Scan可以互相转换;
例子:
一个简单的Exclusive Scan内核
- 适配inclusive的Kogge-Stone扫描内核
- Block 0:
- 线程0将0加载到(共享)$XY[0]$;
- 其他线程将(全局)$X[\mathrm{threadIdx}.x-1]$加载到$XY[\mathrm{threadIdx}.x]
$;
- 所有其他块:
- 所有线程将$X[\mathrm{blockIdx}.x\times \mathrm{blockDim}.x+\mathrm{threadIdx}.x-1]$加载到$XY[\mathrm{threadIdex}.x]$;
- Block 0:
- Brent-Kung内核的类似适配,但注意每个线程加载两个元素;
- 只加载一个零;
- 所有元素都应该只移动一个位置;
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere