ECE408 Lecture 16 Parallel Scan
这次回顾ECE408 Lecture 16,这次介绍了Parallel Scan。
课程主页:
搬运视频:
Inclusive Scan定义
定义:
扫描操作采用二元结合运算符$\oplus$和$n$个元素的数组:
并返回前缀和数组:
例子:
如果$\oplus$是加法,那么在数组$[3,1,7,0,4,1,6,3]$上执行scan操作的结果会返回$[3,4,11,11,15,16,22,25]$。
Scan应用例子
- 假设我们有一块$100$厘米的木头;
- 我们需要以下长度(以厘米为单位)的木块;
- $[3,5,2,7,28,4,3,0,8,1]$;
- 我们如何快速切割这块木头?会剩下多少?
- 方法一:
- 按顺序切割:首先是3,然后是5,接着是2,以此类推;
- 方法二:
- 计算前缀和数组;
- $[3, 8, 10, 17, 45, 49, 52, 52, 60, 61]$(剩下39厘米);
Scan的经典应用
Scan是并行算法的简单和实用的构建块:
将序列算法:
for(j=1; j<n; j++) out[j] = out[j-1] + f(j);
转换为并行算法:
forall(j) { temp[j] = f(j) };
scan(out, temp);
在很多并行算法中都有用:
- Radix sort;
- Quick sort;
- String comparison;
- Lexical analysis;
- Stream compaction;
- Polynomial evaluation;
- Solving recurrences;
- Tree operations;
- Histograms;
- Memory buffer allocation;
顺序扫描
给定序列$[x_0, x_1, x_2, \ldots,]$,计算输出$[y_0, y_1, y_2, \ldots]$,使得:
顺序算法的C实现
y[0] = x[0];
for (i = 1; i < max_i; i++)
y[i] = y[i-1] + x[i];
计算效率:$N$个元素需要$N$次加法,时间复杂度为$O(N)$。
朴素的i并行扫描
- 分配一个线程来计算每个$y$元素;
- 让每个线程将$y$元素所需的所有$x$元素相;
- “只要您不关心性能,并行编程就很容易。”
使用Reduction Tree的并行扫描
- 将每个输出元素计算为所有先前元素的reduction
- 一些reduction部分和将在输出元素的计算中共享;
- 基于IBM的Peter Kogge和Harold Stone在1970年代的硬件附加设计,Kogge-Stone Trees;
Kogge-Stone并行扫描算法展示
Kogge-Stone并行扫描算法说明
每个线程将输入(全局内存)数组中的一个值加载到共享内存数组$T$中,假设$T$的大小是2的幂:
- 对id为stride到$n-1$的线程进行操作,$n - stride$个活跃的线程;
- 线程$j$添加元素$T[j]$和$T[j-stride]$并将结果写入元素$T[j]$
- 每次迭代需要两个同步线程;
syncthreads(); // make sure that input is in place
- 确保输入到位;
float temp = T[j] + T[j-stride];
syncthreads(); // make sure that previous output has been consumed
- 确保所有输入元素都已使用;
T[j] = temp;
部分实现
__global__
void Kogge_Stone_scan_kernel(float *X, float *Y, int InputSize)
{
__shared__ float XY[SECTION_SIZE];
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i < InputSize) XY[threadIdx.x] = X[i];
for (unsigned int stride = 1; stride < blockDim.x; stride *= 2) {
__syncthreads();
if (threadIdx.x >= stride)
// This code has a data race condition
XY[threadIdx.x] += XY[threadIdx.x-stride];
}
Y[i] = XY[threadIdx.x];
}
双缓冲
- 使用两份数据$T_0$和$T_1$;
- 首先使用$T_0$作为输入,$T_1$作为输出;
- 每次迭代后切换输入/输出角色:
- 迭代 0:$T_0$作为输入,$T_1$作为输出;
- 迭代 1:$T_1$作为输入,$T_0$和输出;
- 迭代 2:$T_0$作为输入,$T_1$作为输出;
- 这通常使用两个指针(源和目标)实现,它们将它们的内容从一个迭代交换到下一个迭代;
- 这消除了对第二个
__syncthreads()
调用;
示意图
算法:
- 每次迭代只需要一次
syncthreads()
syncthreads(); // make sure that input is in place
float destination[j] = source[j] + source[j-stride];
temp = destination; destination = source; source = temp;
- 循环后,将目标内容写入全局内存;
工作效率分析
- Kogge-Stone scan内核执$\log(n)$次并行迭代
- 每个步骤执行$(n-1), (n-2), (n-4),\ldots,(n- n/2)$次加操作;
- 添加操作的总数:$n\log n - (n - 1), O(n\log n)$;
- 这种扫描算法工作效率不高
- 顺序扫描算法使用$n$次加法;
- $\log(n)$导致1,000,000个元素时候增加20倍加;
- 所以通常在$n\le 1024$的块中使用;
- 由于工作效率低下导致执行资源饱和时,并行算法可能会变慢;
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere