本笔记主要是阅读:谭升的博客的 GPU编程(CUDA)
【CUDA 基础】1.0 并行计算与计算机架构
并行计算其实设计到两个不同的技术领域:
写并行程序主要是分解任务,一般把一个程序看成是指令和数据的组合,当然并行也可以分为这两种:
我们的任务更加关注数据并行。
任务并行多出现在各种管理系统,比如我们天天用的支付系统,基本上每时每刻都有很多人在同时使用,这时候就需要后台的处理能够并行执行这些请求,不然全国人民排队,那就比春运还热闹了。
数据并行程序设计,第一步就是把数据依据线程进行划分:
块划分,把一整块数据切成小块,每个小块随机的划分给一个线程,每个块的执行顺序随机(关于线程的概念可以去看《深入理解计算机系统》)
thread | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
block | 1 2 3 | 4 5 6 | 7 8 9 | 10 11 12 | 13 14 15 |
周期划分,线程按照顺序处理相邻的数据块,每个线程处理多个数据块,比如我们有五个线程,线程1执行块1,线程2执行块2……线程5执行块5,线程1执行块6
thread | 1 | 2 | 3 | 4 | 5 | 1 | 2 | 3 | 4 | 5 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
block | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
划分不同计算机结构的方法有很多,广泛使用的一种被称为佛林分类法Flynn’s Taxonomy,他根据指令和数据进入CPU的方式分类,分为以下四类:
分别以数据和指令进行分析:
为了提高并行的计算能力,我们要从架构上实现下面这些性能提升:
降低延迟:是指操作从开始到结束所需要的时间,一般用微秒计算,延迟越低越好。
提高带宽:带宽是单位时间内处理的数据量,一般用MB/s或者GB/s表示。
提高吞吐量:是单位时间内成功处理的运算数量,一般用gflops来表示(十亿次浮点计算)。
吞吐量和延迟有一定关系,都是反应计算速度的:
- 一个是时间除以运算次数,得到的是单位次数用的时间–延迟;
- 一个是运算次数除以时间,得到的是单位时间执行次数–吞吐量。
计算机架构也可以根据内存进行划分:
注意:多个处理器可以分多片处理器,和单片多核(众核many-core),也就是有些主板上挂了好多片处理器,也有的是一个主板上就一个处理器,但是这个处理器里面有几百个核。
最后:
【CUDA 基础】1.1 异构计算与CUDA
Abstract: 介绍异构计算和CUDA概述,完成GPU输出Hello world!
Keywords: 异构计算,CUDA
同构计算:是使用相同类型指令集和体系架构的计算单元组成系统的计算方式。
异构计算:主要是指使用不同类型指令集和体系架构的计算单元组成系统的计算方式,常见的计算单元类别包括CPU、GPU、DSP、ASIC、FPGA等。
异构计算用简单的公式可以表示为“CPU+XXX”。
由于术业有专攻,CPU、GPU、DSP、ASIC、FPGA各有所长,在一些场景下,引入特定计算单元,让计算系统变成混合结构,就能让CPU、GPU、DSP、FPGA执行自己最擅长的任务。
GPU原本是不可编程的,或者说不对用户开放的,然后就有hacker开始想办法给GPU编程,来帮助他们完成规模较大的运算,于是他们研究着色语言或者图形处理原语来和GPU对话。后来黄老板发现了这个是个新的功能啊,然后就让人开发了一套平台,CUDA,然后深度学习火了,顺带着,CUDA也火到爆炸。
上面这张图能大致反应CPU和GPU的架构不同。
一个异构应用包含两种以上架构,所以代码也包括不止一部分:
所以主机端的机器码和设备端的机器码是隔离的,自己执行自己的,没办法交换执行。
cudaDeviceReset()
/*
*hello_world.cu
*/
#include
__global__ void hello_world(void)
{
printf("GPU: Hello world!\n");
}
int main(int argc,char **argv)
{
printf("CPU: Hello world!\n");
hello_world<<<1,10>>>();
cudaDeviceReset();//if no this line ,it can not output hello world from gpu
return 0;
}
如果没有cudaDeviceReset()
,则不能正常的运行。
答:这句话包含了隐式同步,GPU和CPU执行程序是异步的,核函数调用后成立刻会到主机线程继续,而不管GPU端核函数是否执行完毕,所以上面的程序就是GPU刚开始执行,CPU已经退出程序了,所以我们要等GPU执行完了,再退出主机线程。
一般CUDA程序分成下面这些步骤:
分配host内存,并进行数据初始化;
分配device内存,并从host将数据拷贝到device上;
调用CUDA的核函数在device上完成指定的运算;
将device上的运算结果拷贝到host上(性能)
释放device和host上分配的内存。
【CUDA 基础】2.0 CUDA编程模型概述(一)
Abstract: 本文介绍CUDA编程模型的简要结构,包括写一个简单的可执行的CUDA程序,一个正确的CUDA核函数,以及相应的调整设置内存,线程来正确的运行程序。
Keywords: CUDA编程模型,CUDA编程结构,内存管理,线程管理,CUDA核函数,CUDA错误处理
CUDA编程模型为应用和硬件设备之间的桥梁,所以CUDA C是编译型语言,不是解释型语言。
GPU中大致可以分为:
从宏观上我们可以从以下几个环节完成CUDA应用开发:
一个异构环境,通常有多个CPU多个GPU,他们都通过PCIe总线相互通信,也是通过PCIe总线分隔开的。所以我们要区分一下两种设备的内存:
注意这两个内存从硬件到软件都是隔离的(CUDA6.0 以后支持统一寻址),我们目前先不研究统一寻址.
一个完整的CUDA应用可能的执行顺序如下图:
从host的串行到调用核函数(核函数被调用后控制马上归还主机线程,也就是在第一个并行代码执行时,很有可能第二段host代码已经开始同步执行了)。
内存管理在传统串行程序是非常常见的:
CUDA程序同样,只是CUDA提供的API可以分配管理设备上的内存,当然也可以用CDUA管理主机上的内存,主机上的传统标准库也能完成主机内存管理。
下面表格有一些主机API和CUDA C的API的对比:
标准C函数 | CUDA C 函数 | 说明 |
---|---|---|
malloc | cudaMalloc | 内存分配 |
memcpy | cudaMemcpy | 内存复制 |
memset | cudaMemset | 内存设置 |
free | cudaFree | 释放内存 |
【CUDA 基础】2.1 CUDA编程模型概述(二)
当主机启动了核函数,控制权马上回到主机,而不是主机等待设备完成核函数的运行。即:所有CUDA核函数的启动都是异步的,这点与C语言是完全不同的。
这一点我们上一篇文章也有提到过(就是等待hello world输出的那段代码后面要加一句)
__global__
/__device__
/__host__
限定符 | 执行 | 调用 | 备注 |
---|---|---|---|
global | 设备端执行 | 可以从主机调用也可以从计算能力3以上的设备调用 | 必须有一个void的返回类型 |
device | 设备端执行 | 设备端调用 | |
host | 主机端执行 | 主机调用 | 可以省略 |
Kernel核函数编写有以下限制
所有编程都需要对错误进行处理,早期的编码错误,编译器会帮我们搞定,内存错误也能观察出来,但是有些逻辑错误很难发现,甚至到了上线运行时才会被发现。
例如我们代码库头文件里面的这个宏:
#define CHECK(call)\
{\
const cudaError_t error=call;\
if(error!=cudaSuccess)\
{\
printf("ERROR: %s:%d,",__FILE__,__LINE__);\
printf("code:%d,reason:%s\n",error,cudaGetErrorString(error));\
exit(1);\
}\
}
就是获得每个函数执行后的返回结果,然后对不成功的信息加以处理,CUDA C 的API每个调用都会返回一个错误代码,这个代码我们就可以好好利用了,当然在release版本中可以去除这部分,但是开发的时候一定要有的。
编译指令:nvcc xxxx.cu -o xxxx
【CUDA 基础】2.2 给核函数计时
我们可以大概分析下核函数启动到结束的过程:
如下图所示:
我们要测试的是2~4的时间。
主要讲了三种:
clock()
:对于核函数计时不准确;gettimeofday
:对于核函数计时较准确;nvprof 工具
:该工具不仅给出了kernel执行的时间,还有其他cuda函数的执行时间。三者之中,最准确。clock()
使用cpu计时的方法是测试时间的一个常用办法,我们在写C程序的时候最多使用的计时方法是:
clock_t start, finish;
start = clock();
// 要测试的部分
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC; // CLOCKS_PER_SEC这个宏,就是每秒中多少clocks,在不同的系统中值可能不同。
不准确原因:
gettimeofday()
具体使用方法:https://linuxhint.com/gettimeofday_c_language/
#include
double cpuSecond() // 例子
{
struct timeval tp;
gettimeofday(&tp,NULL);
return((double)tp.tv_sec+(double)tp.tv_usec*1e-6);
}
gettimeofday()
是linux下的一个库函数,创建一个cpu计时器,从1970年1月1日0点以来到现在的秒数,需要头文件sys/time.h
.
不准确原因:使用gettimeofday()
函数只能测试1~5的时间,所以测试得到的时间偏长。
CUDA 5.0后有一个工具叫做nvprof的命令行分析工具,(后面还要介绍一个图形化的工具)。
nvprof的用法如下:
nvprof [nvprof_args] <application>[application_args]
【CUDA 基础】2.3 组织并行线程
首先,需要知道block、grid三维布局中,x/y/z的先后顺序:
当我们确定好 线程块大小为a时,我们应该如何确定网格的尺寸呢?
答:
在一维中,假设我们需要启动共N个线程,线程块尺寸为a。最容易想到的是,网格尺寸为:N/a
。但是我们需要注意,这可能除不尽(如N=10,a=3),此时算出来的是,网格尺寸为3。如果按照结果来,我们实际上只启动了3*3=9个线程。所以正确的做法是:
在二维中,假设启动共(Nx * Ny)个线程,线程块尺寸为(x, y)。则其网格尺寸:
三维中,以此类推。
一个命令
nvidia-smi
,也可以查询一些信息。nvidia-smi是nvidia驱动程序内带的一个工具,可以返回当前环境的设备信息。具体参数使用使用查文档。
一些函数:
cudaGetDeviceCount()
cudaGetDeviceProperties()
cudaSetDevice()
:设置GPU执行所使用的设备。cudaDriverGetVersion()
:返回驱动程序支持的CUDA的最新版本。cudaRuntimeGetVersion()
:返回CUDA运行时版本。__host__ __device__ cudaError_t cudaGetDeviceCount ( int* count )
返回可计算设备的数量。
Parameters
Returns
__host__ cudaError_t cudaSetDevice ( int device )
设置GPU执行所使用的设备。
Parameters
returns
查询设备步骤:
使用cudaGetDeviceCount()
来获得系统中支持CUDA架构的设备数目;
调用第一步的函数后,我们可以对每个设备进行迭代。CUDA运行时将返回一个cudaDeviceProp类型的结构(具体结构,详见),其中包含了设备的相关属性。
所以,第二步应该:
cudaDeviceProp
类型的变量。如:cudaDeviceProp prop;
cudaGetDeviceProperties ( cudaDeviceProp* prop, int device )
来获得设备信息。在执行完第2步后,该设备的信息以存入变量 prop中。所以,我们可以通过该变量来获取设备具体信息。
// 询问设备信息的主要代码为:
int deviceCount;
cudaGetDeviceCount(&deviceCount);
cudaDeviceProp prop;
for(int i =0; i
https://face2ai.com/CUDA-F-3-1-CUDA%E6%89%A7%E8%A1%8C%E6%A8%A1%E5%9E%8B%E6%A6%82%E8%BF%B0/
Abstract: 本文介绍CUDA执行模型,只比硬件高一层的抽象
Keywords: CUDA SM,SIMT,SIMD,Fermi,Kepler
GPU架构是围绕一个流式多处理器(SM)的扩展阵列搭建的。通过复制这种结构来实现GPU的硬件并行。
上图包括关键组件:
一个GPU含有多个SM;
一个SM支持数百个线程并发执行;
当一个核函数的网格被启动的时候,多个block会被同时分配给可用的SM上执行。
当一个blcok被分配给一个SM后,他就只能在这个SM上执行了,不可能重新分配到其他SM上了;
多个线程块可以被分配到同一个SM上。
SM上同一个块内的多个线程进行线程级别并行;同一线程内,指令利用指令级并行将单个线程处理成流水线。
CUDA 采用单指令多线程SIMT架构管理执行线程;
不同设备有不同的线程束大小,但是到目前为止基本所有设备都是维持在32;
虽然两者都是将相同指令广播给多个执行单元,同一时刻所有线程被分配给相同的指令,但是:
SIMT相比于SIMD,其有以下特征:
而上面这三个特性在编程模型可用的方式就是给每个线程一个唯一的标号(blckIdx,threadIdx),并且这三个特性保证了各线程之间的独立。
因为SM有限,虽然我们的编程模型层面看所有线程都是并行执行的。
但是在微观上看,所有线程块也是分批次的在物理层面的机器上执行,线程块里不同的线程可能进度都不一样,但是同一个线程束内的线程拥有相同的进度。
并行就会引起竞争,多线程以未定义的顺序访问同一个数据,就导致了不可预测的行为,CUDA只提供了一种块内同步的方式,块之间没办法同步!
同一个SM上可以有不止一个常驻的线程束,有些在执行,有些在等待,他们之间状态的转换是不需要开销的。即:每个线程束在同一时间执行同一指令,同一个块内的线程束互相切换是没有时间消耗的。
这一小节请阅读:https://face2ai.com/CUDA-F-3-1-CUDA%E6%89%A7%E8%A1%8C%E6%A8%A1%E5%9E%8B%E6%A6%82%E8%BF%B0/#Fermi-架构
这一节以Fermi架构作为硬件基础。
每个SM有两个线程束调度器,两个指令调度单元。
当一个线程块被指定给一个SM时,线程块内的所有线程被分成warp,两个线程束调度器会选择其中的两个线程束(warp),然后用指令调度器存储两个线程束要执行的指令。这些线程束在SM上交替执行。