ECE408 Lecture 18 Atomic Operations and Histogramming
这次回顾ECE408 Lecture 18,这次介绍了原子操作和直方图。
课程主页:
搬运视频:
目标
- 理解原子操作
- 并行计算中的读取-修改-写入;
- 并行程序中“临界区”的原始形式;
- 在CUDA中使用原子操作;
- 为什么原子操作会降低内存系统的吞吐量;
- 如何避免某些并行算法中的原子操作;
- 直方图作为原子操作的示例应用
- 基本的直方图算法;
- 私有化;
一种常见模式
- 多个客户预订机票;
- 每个客户:
- 调出飞行座位图;
- 决定座位;
- 更新座位图,将座位标记为已占用;
- 糟糕的结果:多名乘客最终预订了同一个座位;
Read-Modify-Write操作
如果Mem[x]
最初为0,那么在线程1和2完成后Mem[x]
的值是多少?
- 每个线程在它们的Old变量中得到什么?
答案可能因数据竞争而有所不同。为了避免数据竞争,我们需要使用原子操作。
分析
场景1:
- Thread 1 Old = 0;
- Thread 2 Old = 0;
- Mem[x] = 2;
场景2:
- Thread 1 Old = 0;
- Thread 2 Old = 0;
- Mem[x] = 2;
场景3:
- Thread 1 Old = 0;
- Thread 2 Old = 0;
- Mem[x] = 1;
场景4:
- Thread 1 Old = 0;
- Thread 2 Old = 0;
- Mem[x] = 1;
原子操作——确保正确的结果
一般的原子操作
- 通常由处理器指令集中的单个指令在内存位置地址上执行;
- 读取旧值,计算新值,并将新值写入该位置;
- 硬件确保在原子操作完成之前没有其他线程可以访问该位置;
- 访问该位置的任何其他线程通常会停止或保留在队列中,直到轮到它;
- 所有线程串行执行原子操作;
CUDA的原子操作
- 被翻译成单条指令的函数调用(也叫intrinsics )
- 原子add、sub、inc、dec、min、max、exch(交换)、CAS(比较和交换);
- 阅读CUDA C编程指南了解更多详情;
- 原子添加
int atomicAdd(int* address, int val);
读取全局或共享内存中地址指向的32位字old
,计算(old + val)
,并将结果存储回同一地址的内存。该函数返回old
。
CUDA中更多的原子加法
- Unsigned 32-bit:
unsigned int atomicAdd(unsigned int* address, unsigned int val);
- Unsigned 64-bit:
unsigned long long int atomicAdd(unsigned long long int* address, unsigned long long int val);
- 单精度浮点数加法:
float atomicAdd(float* address, float val);
与原子建立同步
- 我们如何为block构建
__syncthreads()
; - 我们如何为整个grid创建
__syncthreads()
?- 为什么这不是一个好主意?
- 我们如何创建临界区? 即,每个block的每个线程执行一段特定的代码?
- 我们如何为每个grid创建一个临界区?
- 为什么这与grid的
__syncthreads()
没有相同的问题?
- 为什么这与grid的
原子Compare and Swap
int atomicCAS(int *address, int compare, int val)
{
int old = *address;
if (old == compare)
*address = val;
return old;
}
直方图
- 一种从大型数据集中提取显着特征和模式的方法;
- 用于图像中对象识别的特征提取;
- 信用卡交易中的欺诈检测;
- 在天体物理学中关联恒星物体运动;
- 基本直方图——对于数据集中的每个元素,使用值来确定要递增的“bin”;
直方图示例
- 在句子“Programming Massively Parallel Processors”中构建每个字母的频率直方图
- A(4)、C(1)、E(1)、G(1);
- 如何并行执行此操作?
更好的方法
- 合并读取
- 以strided模式为每个线程分配输入;
- 相邻的线程处理相邻的输入字母;
所有线程移动到输入的下一部分:
Kernel
__global__
void histo_kernel(unsigned char *buffer,int size, unsigned int *histo)
{
int i = threadIdx.x + blockIdx.x * blockDim.x;
// stride is total number of threads
int stride = blockDim.x * gridDim.x;
while (i < size) {
atomicAdd( &(histo[buffer[i]]), 1);
i += stride;
}
}
DRAM上的原子操作
一个原子操作从读开始,有几百个周期的延迟:
一个原子操作从读开始,有几百个周期的延迟;原子操作以写结束,有几百个周期的延迟;在此期间,没有其他人可以访问该位置。
每个Load-Modify-Store都有两个完整的内存访问延迟,对同一变量(RAM 位置)的所有原子操作都被序列化:
延迟决定了原子操作的吞吐量
- 原子操作的吞吐量是应用程序可以在特定位置执行原子操作的速率;
- 速率受read-modify-write序列的总延迟限制,对于全局内存 (DRAM) 位置通常超过1000个周期;
- 这意味着如果许多线程试图在同一位置执行原子操作(竞争),内存带宽将减少到 < 1/1000;
硬件提升
- L2 缓存上的原子操作;
- 中等延迟,但仍然序列化;
- 全局到所有块;
- 全局内存原子的“免费改进”;
- 共享内存上的原子操作
- 非常短的延迟,但仍然是序列化的;
- 每个线程块私有;
- 需要程序员的算法工作;
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere