第一章 指针篇
第二章 cuda原理篇
第三章 CUDA编译器环境配置篇
第四章 核函数基础篇
第五章 kernel索引方法篇
第六章 kenel计算应用实战篇
…
随着人工智能的发展与人才的内卷,很多企业已将深度学习算法的C++部署能力作为基本技能之一。面对诸多arm相关且资源有限的设备,往往想更好的提速,满足更高时效性,必将更多类似矩阵相关运算交给CUDA处理。同时,面对市场诸多教程与诸多博客岑子不起的教程或高昂教程费用,使读者(特别是小白)容易迷糊,无法快速入手CUDA编程,实现工程化。
因此,我将结合我的工程实战经验,我将在本专栏实现CUDA系列教程,帮助读者(或小白)实现CUDA工程化,掌握CUDA编程能力。学习我的教程专栏,你将绝对能实现CUDA工程化,完全从环境安装到CUDA核函数编程,从核函数到使用相关内存优化,从内存优化到深度学习算子开发(如:nms),从算子优化到模型(以yolo系列为基准)部署。最重要的是,我的教程将简单明了直切主题,CUDA理论与实战实例应用,并附相关代码,可直接上手实战。我的想法是掌握必要CUDA相关理论,去除非必须繁杂理论,实现CUDA算法应用开发,待进一步提高,将进一步理解更高深理论。
欲速则不达,工程化编程也需理解其工作本质,本篇带领读者了解CUDA工作原理等知识。
CPU(Central Processing Unit):是一块超大规模的集成电路,是一台计算机的运算核心(Core)和控制核心( Control Unit)。它的功能主要是解释计算机指令以及处理计算机软件中的数据。CPU与内部存储器(Memory)和输入/输出(I/O)设备合称为电子计算机三大核心部件。CPU主要包括运算器(算术逻辑运算单元,ALU,Arithmetic Logic Unit)、控制单元(CU, Control Unit)、寄存器(Register)、和高速缓冲存储器(Cache)及实现它们之间联系的数据(Data)、控制及状态的总线(Bus)。简单来说就是:计算单元、控制单元和存储单元。CPU遵循的是冯诺依曼架构,其核心就是:存储程序,顺序执行。CPU的架构中需要大量的空间去放置存储单元和控制单元,相比之下计算单元只占据了很小的一部分,所以它在大规模并行计算能力上极受限制,而更擅长于逻辑控制。
GPU(Video card,Graphics card):全称显示接口卡,又称显示适配器,是计算机最基本配置、最重要的配件之一。显卡是电脑进行数模信号转换的设备,承担输出显示图形的任务。具体来说,显卡接在电脑主板上,它将电脑的数字信号转换成模拟信号让显示器显示出来,同时显卡还是有图像处理能力,可协助CPU工作,提高整体的运行速度。在科学计算中,显卡被称为显示加速卡。2006年,NVIDIA公司发布了CUDA,CUDA是建立在NVIDIA的CPUs上的一个通用并行计算平台和编程模型,基于CUDA编程可以利用GPUs的并行计算引擎来更加高效地解决比较复杂的计算难题。GPU并不是一个独立运行的计算平台,而需要与CPU协同工作,可以看成是CPU的协处理器,因此当我们在说GPU并行计算时,其实是指的基于CPU+GPU的异构计算架构。在异构计算架构中,GPU与CPU通过PCIe总线连接在一起来协同工作,CPU所在位置称为为主机端(host),而GPU所在位置称为设备端(device),如下图所示。
GPU中有很多的运算器ALU和很少的缓存cache,缓存的目的不是保存后面需要访问的数据的,这点和CPU不同,而是为线程thread提高服务的。如果有很多线程需要访问同一个相同的数据,缓存会合并这些访问,然后再去访问DRAM。
CUDA的全称是Computer Unified Device Architecture(计算机统一设备架构):是NVIDIA推向市场的并行计算架构,CUDA作为NVIDIA图形处理器的通用计算引擎,提供给我们利用NVIDIA显卡进行GPGPU(General Purpose Graphics Process Unit)开发的全套工具。
CUDA在软件方面组成有:一个CUDA库、一个应用程序编程接口(API)及其运行库(Runtime)、两个较高级别的通用数学库,即CUFFT和CUBLAS。CUDA改进了DRAM的读写灵活性,使得GPU与CPU的机制相吻合。另一方面,CUDA提供了片上(on-chip)共享内存,使得线程之间可以共享数据。应用程序可以利用共享内存来减少DRAM的数据传送,更少的依赖DRAM的内存带宽。
上图为典型的CUDA程序的执行流程图,CUDA执行流程中最重要的一个过程是调用CUDA的核函数来执行并行计算。在CUDA程序构架中,主机端代码在CPU上执行,是类似普通的C代码;若需数据并行处理,可使用CUDA编译成GPU能执行的程序,并传送到GPU设备端,在GPU上执行,此代码是cuda编写为.cu文件,即我们后续将讲解的编码方式。
本部分将介绍CUDA编码的过程及使用模块。
CUDA编程基本是核函数进行编码,结构如下图,典型cuda程序流程如下:
1.分配host内存,并进行数据初始化;
2.分配device内存,并从host将数据拷贝到device上;
3.调用CUDA的核函数在device上完成指定的运算;
4.将device上的运算结果拷贝到host上;
5.释放device和host上分配的内存。
重点说明(如下图):x为下面的0,1,2,3,4;y为0,0,0,0,0
1.主机(Host):将CPU及系统的内存(内存条)称为主机。
2.设备(Device):将GPU及GPU本身的显示内存称为设备。
3.动态随机存取存储器(DRAM):Dynamic Random Access Memory,最为常见的系统内存。DRAM只能将数据保持很短的时间。为了保持数据,DRAM使用电容存储,所以必须隔一段时间刷新(refresh)一次,如果存储单元没有被刷新,存储的信息就会丢失。
CUDA编程是一个多线程编程,数个线程(Thread)组成一个线程块(Block),所有线程块组成一个线程网格(Grid),其中网格和线程块只是逻辑划分,SM才是执行的物理层。
1.网格 grid:kernel在device上执行时,实际上是启动很多线程,一个kernel所启动的所有线程称为一个网格(grid),同一个网格上的线程共享相同的全局内存空间。
2.线程块 block:网格又可以分为很多线程块(block),一个block里面包含很多线程。各block是并行执行的,block间无法通信,也没有执行顺序。每个block有包含共享内存(Shared Memory),可以被线程块中所有线程共享,其生命周期与线程块一致。
3.线程 thread: 一个CUDA的并行程序会被以许多个threads来执行。数个threads会被群组成一个block,同一个block中的threads可以同步,也可以通过shared memory通信。
4.线程束 warp:GPU执行程序时的调度单位,SM的基本执行单元。目前在CUDA架构中,warp是一个包含32个线程的集合,这个线程集合被“编织在一起”并且“步调一致”的形式执行。同一个warp中的每个线程都将以不同数据资源执行相同的指令,这就是所谓 SIMT架构(Single-Instruction, Multiple-Thread,单指令多线程)。仅了解
内存模型,如下图所示。可以看到,每个线程有自己的私有本地内存(Local Memory),而每个线程块有包含共享内存(Shared Memory),可以被线程块中所有线程共享,其生命周期与线程块一致。此外,所有的线程都可以访问全局内存(Global Memory)。还可以访问一些只读内存块:常量内存(Constant Memory)和纹理内存(Texture Memory)。
Registers:寄存器是GPU中最快的memory,kernel中没有什么特殊声明的自动变量都是放在寄存器中的。当数组的索引是constant类型且在编译期能被确定的话,就是内置类型,数组也是放在寄存器中。 寄存器变量是每个线程私有的,一旦thread执行结束,寄存器变量就会失效。
Shared Memory:用__shared__修饰符修饰的变量存放在shared memory中。Shared Memory位于GPU芯片上,访问延迟仅次于寄存器。所有Thread来进行访问的,可以实现Block内的线程间的低开销通信。 要使用__syncthread()同步。
Local Memory:本身在硬件中没有特定的存储单元,而是从Global Memory虚拟出来的地址空间。是为寄存器无法满足存储需求的情况而设计的,主要是用于存放单线程的大型数组和变量。Local Memory是线程私有的,线程之间是不可见的。它的访问是比较慢的,跟Global Memory的访问速度是接近的。使用情景,无法确定其索引是否为常量的数组;会消耗太多寄存器空间的大型结构或数组;如果内核使用了多于寄存器的任何变量(这也称为寄存器溢出);
Constant Memory:固定内存空间驻留在设备内存中,并缓存在固定缓存中(constant cache),范围是全局的,针对所有kernel; kernel只能从constant Memory中读取数据,因此其初始化必须在host端使用下面的function调用:cudaError_t cudaMemcpyToSymbol(const void* symbol,const void* src,size_t count); 当一个warp中所有线程都从同一个Memory地址读取数据时,constant Memory表现会非常好,会触发广播机制。
Global Memory:Global Memory在某种意义上等同于GPU显存,kernel函数通过Global Memory来读写显存。Global Memory是kernel函数输入数据和写入结果的唯一来源。
Texture Memory:是GPU的重要特性之一,也是GPU编程优化的关键。Texture Memory实际上也是Global Memory的一部分,但是它有自己专用的只读cache。这个cache在浮点运算很有用,Texture Memory是针对2D空间局部性的优化策略,所以thread要获取2D数据就可以使用texture Memory来达到很高的性能。从读取性能的角度跟Constant Memory类似。
Host Memory:主机端存储器主要是内存可以分为两类:可分页内存(Pageable)和页面 (Page-Locked 或 Pinned)内存。可分页内存通过操作系统 API(malloc/free) 分配存储器空间,该内存是可以换页的,即内存页可以被置换到磁盘中。可分页内存是不可用使用DMA(Direct Memory Acess)来进行访问的,普通的C程序使用的内存就是这个内存。
GPU存在很多CUDA核心,充分利用CUDA核心可以充分发挥GPU的并行计算能力,cuda并行计算过程主要依赖SP与SM结构,结构图下图。接下来,我将介绍其过程:
SP(streaming processor):最基本的处理单元,也称为CUDA core。最后具体的指令和任务都是在SP上处理的。GPU进行并行计算,也就是很多个SP同时做处理,SP的主要结构为一个ALU(逻辑运算单元),一个FPU(浮点运算单元)以及一个Register File(寄存器堆)。
SM(Streaming Multiprocessor):GPU硬件的一个核心组件是流式多处理器。SM的核心组件包括CUDA核心、共享内存、寄存器等,SM可以并发地执行数百个线程,并发能力就取决于SM所拥有的资源数。
暂时大家先记住CUDA编程的并行计算是使用核函数,即kernel函数上执行。当一个kernel被执行时,它的gird中的线程块block被分配到SM上,一个线程块只能在一个SM上被调度
。SM一般可以调度多个线程块,这要看SM本身的能力。那么有可能一个kernel的各个线程块被分配多个SM,所以grid只是逻辑层,而SM才是执行的物理层。SM采用的是SIMT (Single-Instruction, Multiple-Thread,单指令多线程)架构,基本的执行单元是线程束(warps),线程束包含32个线程
,这些线程同时执行相同的指令,但是每个线程都包含自己的指令地址计数器和寄存器状态,也有自己独立的执行路径。所以尽管线程束中的线程同时从同一程序地址执行,但是可能具有不同的行为,比如遇到了分支结构,一些线程可能进入这个分支,但是另外一些有可能不执行,它们只能死等,因为GPU规定线程束中所有线程在同一周期执行相同的指令,线程束分化会导致性能下降
。当线程块被划分到某个SM上时,它将进一步划分为多个线程束,因为这才是SM的基本执行单元,但是一个SM同时并发的线程束数是有限的。这是因为资源限制,SM要为每个线程块分配共享内存,也要为每个线程束中的线程分配独立的寄存器。所以SM的配置会影响其所支持的线程块和线程束并发数量
。
总之,就是网格grid和线程块block只是逻辑划分,一个kernel的所有线程其实在物理层是不一定同时并发的。所以kernel的grid和block的配置不同,性能会出现差异
,这点是要特别注意的。还有,由于SM的基本执行单元是包含32个线程的线程束,所以block大小一般要设置为32的倍数。
GeForce9300M GS只拥有1个SM) Thread是CUDA模型中最基本的运行单元,执行最基本的程序指令。Block是一组协作Thread,Block内部允许共享存储,每个Block最多包含512个Thread。Grid是一组Block,共享全局内存。Kernel是在GPU上执行的核心程序,每一个Grid对应一个Kernel任务。 在程序运行的时候,实际上每32个Thread组成一个Warp,每个 warp 块都包含连续的线程,递增线程 ID 。Warp是SM的基本调度单位,每次运行的时候,由于SM数量不同,所以一个Block内的所有Thread不一定全部同时运行,但是每个Warp内的所有Thread一定同时运行。因此,我们在定义Thread Size的时候应使其为Warp Size的整数倍,也就是Thread Size应为32的整数倍。理论上Thread越多,就越能弥补单个Thread读取数据的latency ,但是当Thread越多,每个Thread可用的寄存器也就越少,严重的时候甚至能造成Kernel无法启动。因此每个Block最少应包含64个Thread,一般选择128或者256,具体视SM数目而定。一个SM最多可以同时运行768个Thread,但每个SM最多包含8个Block,因此要保持100%利用率,Block数目与其Size有如下几种设定方式
以下代码可查询GPU相关能力:
void get_gpu_info() {
int dev = 0;
cudaDeviceProp devProp;
cudaGetDeviceProperties(&devProp, dev);
std::cout << "使用GPU device " << dev << ": " << devProp.name << std::endl;
std::cout << "SM的数量:" << devProp.multiProcessorCount << std::endl;
std::cout << "每个线程块的共享内存大小:" << devProp.sharedMemPerBlock / 1024.0 << " KB" << std::endl;
std::cout << "每个线程块的最大线程数:" << devProp.maxThreadsPerBlock << std::endl;
std::cout << "每个EM的最大线程数:" << devProp.maxThreadsPerMultiProcessor << std::endl;
std::cout << "每个SM的最大线程束数:" << devProp.maxThreadsPerMultiProcessor / 32 << std::endl;
}