cuda_learning_02
cuda_learning_02
内容主要来自知乎文章,CUDA(二):GPU的内存体系及其优化指南,本文是学习笔记。
一、GPU的内存体系
各级内存及其特点
全局内存(global memory)
最大,延迟最高、最长使用的内存,常说的“显存”中的大部分都是全局内存。可以用 cudaMemcpy
函数将主机的数据复制到全局内存,或者反过来。
常量内存(constant memory)
常量内存是指存储在片下存储的设备内存,但是通过特殊的常量内存缓存(constant cache)进行缓存读取,常量内存为只读内存。 常量内存数量有限,一共仅有 64 KB,由于有缓存,常量内存的访问速度比全局内存高,但得到高访问速度的前提是一个线程束中的线程(一个线程块中相邻的 32 个线程)要读取相同的常量内存数据。
一个使用常量内存的方法是在核函数外面用 __constant__
定义变量,并用 API 函数 cudaMemcpyToSymbol
将数据从主机端复制到设备的常量内存后 供核函数使用。
纹理内存(texture memory)和表面内存(surface memory)
纹理内存和表面内存类似于常量内存,也是一 种具有缓存的全局内存,有相同的可见范围和生命周期,而且一般仅可读(表面内存也可 写)。不同的是,纹理内存和表面内存容量更大,而且使用方式和常量内存也不一样。
寄存器(register)
寄存器是线程能独立访问的资源,它所在的位置与局部内存不一样,是在片上(on chip)的存储,用来存储一些线程的暂存数据。寄存器的速度是访问中最快的,但是它的容量较小。
在核函数中定义的不加任何限定符的变量一般来说就存放于寄存器(register)中。 各种内建变量,如 gridDim、blockDim、blockIdx、 threadIdx 及 warpSize 都保存在特殊的寄存器中,以便高效访问。
寄存器变量仅仅被一个线程可见,寄存器的生命周期也与所属线程的生命周期 一致,从定义它开始,到线程消失时结束。
局部内存(local memory)
局部内存和寄存器几乎一 样,核函数中定义的不加任何限定符的变量有可能在寄存器中,也有可能在局部内存中。寄存器中放不下的变量,以及索引值不能在编译时就确定的数组,都有可能放在局部内存中。
虽然称之为“局部内存”,但是其本质是设备全局内存(但不同于全局内存)中为每个线程单独分配的一块内存。所以,局部内存的延迟也很高,每个线程最多能使用高达512 KB的局部内存,但使用过多会降低程序的性能。
共享内存(shared memory)
共享内存和寄存器类似,存在于芯片上,具有仅次于寄存器的读写速度,数量也有限。 一个使用共享内存的变量可以 __shared__
修饰符来定义。
共享内存对整个线程块可见,其生命周期也与整个线程块一致。共享内存的主要作用是减少对全局内存的访问,或者改善对全局内存的访问模式。
L1和L2 缓存
每个 SM 都有一个 L1 缓存,所有 SM 共享一个 L2 缓存。L1 和 L2 缓存都被用来存储局部内存和全局内存中的数据,也包括寄存器中溢出的部分,以减少延时。
从物理结构上来说,在最新的GPU架构中,L1 缓存、纹理缓存及共享内存三者是统一的。但从编程的角度来看,共享内存是可编程的缓存(共享内存的使用完全由用户操控),而L1 和 L2 缓存是不可编程的缓存(用户最多能引导编译器做一些选择)。
SM 构成及典型GPU的对比
一个 GPU 是由多个 SM 构成的。一个 SM 包含如下资源:
一定数量的寄存器。
一定数量的共享内存。
常量内存的缓存。
纹理和表面内存的缓存。
L1缓存。
线程束调度器(warp scheduler) 。
执行核心,包括:
- 若干整型数运算的核心(INT32) 。
- 若干单精度浮点数运算的核心(FP32) 。
- 若干双精度浮点数运算的核心(FP64) 。
- 若干单精度浮点数超越函数(transcendental functions)的特殊函数单元(Special Function Units,SFUs)。
- 若干混合精度的张量核心(tensor cores)
单精度浮点数超越函数(transcendental functions) 的 特殊函数单元(Special Function Units,SFUs) 是指一种硬件单元,它专门用于执行一些数学上超越(超出普通代数运算)函数的计算,如三角函数(sin, cos),指数函数(exp),对数函数(log),平方根(sqrt)等。
张量核心(Tensor Cores):主要用于加速深度学习中的矩阵运算,尤其是低精度浮点数运算(如 FP16)。
L1 Instruction Cache
用于缓存线程束(warp)的指令,提高指令读取的效率。
Warp Scheduler 和 Dispatch Unit
Warp Scheduler(线程束调度器):
每个 SM 中有多个 Warp Scheduler,每个调度器可以每个时钟周期调度 32 个线程(一个 Warp)。调度线程束执行任务,包括加载指令、分配执行单元等。
Dispatch Unit(指令派发单元):
Warp Scheduler 将指令分配给不同的执行单元(如 FP32、FP64、INT32 核心或 SFUs 等),由 Dispatch Unit 具体派发。
Register File(寄存器文件)
执行核心
L0 Instruction Cache 和数据缓存
L0 Instruction Cache:
每个 Warp Scheduler 附带的更小的指令缓存,用于加速最近使用的指令。
L1 Data Cache / Shared Memory:
每个 SM 配备 256 KB 的共享内存或 L1 数据缓存。
Load/Store 单元
负责从全局内存中加载数据或将计算结果存储到全局内存中。
纹理单元(Tex)
纹理单元专门用于处理纹理数据加载,通常在图形渲染中使用。
Tensor Memory Accelerator
针对张量核心操作的特殊加速器,用于处理张量内存的加载和存储。
GPU 之外:近存计算与存算一体
在GPU的层次结构之外,为了降低访存成本,获得更高的性能,近存计算与存算一体逐渐成为热门的方向。
近存计算: Graphcore IPU
存算一体: 后摩智能 H30
存算一体或者存内计算的核心思想是,通过对存储器单元本身进行算法嵌入,使得计算可以在存储器单元内完成。
二、通过规约(Reduction)操作理解GPU内存体系
关于reduce优化,CUDA(二):GPU的内存体系及其优化指南(blog1)和深入浅出GPU优化系列:reduce优化(blog2)都做出了很详细的讲解,两篇文章都写得非常详细,优化的角度也是大同小异的,下面的内容主要是整理两篇文章中提到优化方向,并对所有提到优化角度做一个小小的总结。
首先,算法reduce即求解$x=x_0 \bigotimes x1 \bigotimes x_2 \bigotimes x_3 \bigotimes … \bigotimes x_n$。其中$\bigotimes$可表示为求sum,min,max,avg等操作,最后获得的输出相比于输入一般维度上会递减。在GPU中,reduce采用了一种树形的计算方式,并且由于GPU没有针对global数据的同步操作,只能针对block的数据进行同步。所以,reduce一般分为两个阶段。
blog1
1. cpu 版本
1 | real reduce(const real *x, const int N) |
1 | sum = 33554432.000000. |
2. 仅使用全局内存
每个线程负责其唯一id对应的那个位置的值的计算,N=1e8,每个线程块有128个线程。
1 | void __global__ reduce_global(real *d_x, real *d_y) |
3.
笔记
技巧1:解决warp divergence
确实能够解决warp divergence,只有当工作线程小于32的时候才会出现warp divergence。需要注意的是这份代码里每个线程并不是负责计算自己的唯一id对应的那个位置的数字每次迭代后的值。
技巧2:解决bank冲突
确实会存在bank冲突,也确实是第一次迭代是2路冲突,第二次是4路,再是8路,16路,这是因为bank冲突是针对一个wrap里的线程说的。
解决方法其实就是从大到小枚举offset。