Caffe 初学拾遗(四) CUDA 框架说明

Original Source:

http://bbs.csdn.net/topics/390798229

http://blog.csdn.net/augusdi/article/details/12833235

在前人基础上,进行了精简和勘误,意在形成对CUDA编程的概念性理解:


推荐书目:

《GPU高性能编程CUDA实战》(译作) 原书名为an introduction of General-purpose GPU Programming


What is it?

CUDA( Compute Unified Device Architecture )

由NVIDIA公司创立的,基于NVIDIA生产的图形处理器GPUs(Graphics Processing Units)的并行计算平台和编程模型。

Why CUDA?

通过CUDA,GPUs可以很方便地被用来进行通用计算(类似于在CPU中进行的数值计算)。

将并行计算由大型集群扩展到了普通显卡,使得用户只需要一台带有Geforce显卡的计算机就能运行较大规模的并行处理程序。

相比于CPU,GPUs的FLOPS(float-point Operations Per Second)是CPU的数倍。


Framework:

Host:将CPU及内存称为主机。

Device:将GPU及GPU显存称为设备。

在 CUDA 的架构下,一个程序分为两个部分:host 端和 device 端。

Host 端是指在 CPU 上执行的部分,而 Device 端则是GPU在上执行的部分。Device 端的程序又称为 "Kernel"。

通常 Host 端程序会将数据准备好后,复制到显卡的显存中,再由显示芯片执行 Device 端程序,完成后再由 Host 端程序将结果从显存中取回。

{Note:由于 CPU 存取显存时只能通过 PCI Express 接口,因此速度较慢(PCI Express x16 的理论带宽是双向 4GB/s),因此不能经常进行这类动作,以免降低效率。}

Thread:显存执行操作最小单位,一般通过GPU的一个核进行处理。
Block: 
1. 数个 thread 可以组成一个 Block,存取同一块共享的内存
2. 各Block是并行执行的,Block间无法通信,也没有执行顺序。
3. 注意线程块的数量限制为不超过65535(硬件限制)。
Grid:执行相同程序的 Block,可以组成 Grid。

线程束
在CUDA架构中,线程束是指一个包含32个线程的集合,这个线程集合被“编织在一起”并且以“步调一致”的形式执行。在程序中的每一行,线程束中的每个线程都将在不同数据上执行相同的命令
Kernel
1. 在GPU上执行的函数通常称为核函数(Kernel)。
2. 一般通过标识符__global__修饰,调用通过<<<参数1,参数2>>>,用于说明内核函数中的线程数量,以及线程是如何组织的。
3. 以线程格(Grid)的形式组织,每个线程格(Grid)由若干个线程块(block)组成,而每个线程块又由若干个线程(thread)组成
4. 是以block为单位执行的。
5. 在主机端(Host)代码中调用。
6. 在调用时必须声明内核函数的执行参数。
7. 在编程时,必须先为Kernel函数中用到的数组或变量分配好足够的空间,再调用Kernel函数,否则在GPU计算时会发生错误,例如越界或报错。

E.g.

/*
 * @file_name HelloWorld.cu  后缀名称.cu
 */
 
#include 
#include   //头文件
 
//核函数声明,前面的关键字__global__
__global__ void kernel( void ) {
}
 
int main( void ) {
    //核函数的调用,注意<<<1,1>>>,第一个1,代表线程格里只有一个线程块;第二个1,代表一个线程块里只有一个线程。
    kernel<<<1,1>>>();
    printf( "Hello, World!\n" );
    return 0;
}


Details:

dim3数据类型:
1. dim3是基于uint3定义的矢量类型,相当于由3个unsigned int型组成的结构体。
uint3类型有三个数据成员unsigned int x; unsigned int y; unsigned int z;

2. 可使用一维、二维或三维的索引来标识线程,构成一维、二维或三维线程块。
3. dim3结构类型变量用于核函数调用的<<<,>>>中
dim3内置变量:
1. threadIdx,获取线程thread的ID索引;如果线程是一维的那么就取threadIdx.x,二维的还可以多取到一个值threadIdx.y,以此类推到三维threadIdx.z。
2. blockIdx,线程块的ID索引;同样有blockIdx.x,blockIdx.y,blockIdx.z。
3. blockDim,线程块的维度,同样有blockDim.x,blockDim.y,blockDim.z。
4. gridDim,线程格的维度,同样有gridDim.x,gridDim.y,gridDim.z。
Note:        

1. 对于一维的block,线程的threadID == threadIdx.x。
2. 对于大小为(blockDim.x, blockDim.y)的二维 block,线程的threadID=threadIdx.x+threadIdx.y*blockDim.x
3. 对于大小为(blockDim.x, blockDim.y, blockDim.z)的三维 block,线程的threadID=threadIdx.x+threadIdx.y*blockDim.x+threadIdx.z*blockDim.x*blockDim.y
4. 对于计算线程索引偏移增量为已启动线程的总数。如stride = blockDim.x * gridDim.x; threadId += stride


函数修饰符:
1. __global__,表明被修饰的函数在Device上执行,但在Host上调用。
2. __device__,表明被修饰的函数在Device上执行,但只能在其他__device__函数或者__global__函数中调用。


GPU Functions:

cudaMalloc()
1. 函数原型: cudaError_t cudaMalloc (void **devPtr, size_t size)
2. 函数用法:与C语言中的malloc函数一样,在GPU中分配显存。
Note
1. 可以将cudaMalloc()返回的指向已分配地址空间的指针传递给在设备上执行的函数;
2. 可以在__device__代码中使用cudaMalloc()返回的指针进行设备内存读写操作;
3. 可以将cudaMalloc()返回的指针传递给在Host上执行的函数;
4. 不可以在主机代码中使用cudaMalloc()返回的指针进行Host的内存读写操作(即不能进行解引用)。

cudaMemcpy()
1. 函数原型:cudaError_t cudaMemcpy (void *dst, const void *src, size_t count, 
cudaMemcpyKind kind)

2. 函数用法:与c语言中的memcpy函数一样,只是此函数在Host内存与GPU显存之间互相拷贝数据。
3. 函数参数:cudaMemcpyKind kind表示数据拷贝方向,如果kind赋值为cudaMemcpyDeviceToHost表示数据从Device显存拷贝到Host内存。
4. 与C中的memcpy()一样,以同步方式执行,即当函数返回时,复制操作就已经完成了,并且在输出缓冲区中包含了复制进去的内容。
5. 相应的有个异步方式执行的函数cudaMemcpyAsync(),这个函数详解请看下文有关内容。

cudaFree()
1. 函数原型:cudaError_t cudaFree ( void* devPtr )。
2. 函数用法:与c语言中的free()函数一样,只是此函数释放的是cudaMalloc()分配的内存。
E.g.

#include 
#include 
__global__ void add( int a, int b, int *c ) {
    *c = a + b;
}
int main( void ) {
    int c;
    int *dev_c;
    //cudaMalloc()
    cudaMalloc( (void**)&dev_c, sizeof(int) );
    //核函数执行
    add<<<1,1>>>( 2, 7, dev_c );   
    //cudaMemcpy()
    cudaMemcpy( &c, dev_c, sizeof(int),cudaMemcpyDeviceToHost ) ;
    printf( "2 + 7 = %d\n", c );
    //cudaFree()
    cudaFree( dev_c );
 
    return 0;
}

GPU Memory Classification:

Shared Memory:
1. 位置:Device Memory(显存)。
2. 形式:关键字__shared__添加到变量声明中。如__shared__ float cache[10]。
3. 目的:对于GPU上启动的每个线程块,CUDA C编译器都将创建该共享变量的一个副本。线程块中的每个线程都共享这块内存,但线程却无法看到也不能修改其他线程块的变量副本。这样使得一个线程块中的多个线程能够在计算上通信和协作。


Constant Memory:
1. 位置:Device Memory(显存)。
2. 形式:关键字__constant__添加到变量声明中。如__constant__ float s[10];。
3. 目的:为了提升性能。常量内存采取了不同于标准全局内存的处理方式。在某些情况下,用常量内存替换全局内存能有效地减少内存带宽。
4. 特点:常量内存用于保存在核函数执行期间不会发生变化的数据。变量的访问限制为只读。NVIDIA硬件提供了64KB的常量内存。不再需要cudaMalloc()或者cudaFree(),而是在编译时,静态地分配空间。
5. 要求:当我们需要拷贝数据到常量内存中应该使用cudaMemcpyToSymbol(),而cudaMemcpy()会复制到全局内存。
6. 性能提升的原因:
6.1. 对常量内存的单次读操作可以广播到其他的“邻近”线程。这将节约15次读取操作。(“邻近”指半个线程束,一个线程束包含32个线程的集合。)
6.2. 常量内存的数据将缓存起来,因此对相同地址的连续读操作将不会产生额外的内存通信量。


Fixed Memory:
1. 位置:Host Memory(内存)。
2. 概念:也称为页锁定内存或者不可分页内存,操作系统将不会对这块内存分页并交换到磁盘上,从而确保了该内存始终驻留在物理内存中。因此操作系统能够安全地使某个应用程序访问该内存的物理地址,因为这块内存将不会破坏或者重新定位。
3. 目的:提高访问速度。由于GPU知道主机内存的物理地址,因此可以通过“直接内存访问DMA(Direct Memory Access)技术来在GPU和主机之间复制数据。由于DMA在执行复制时无需CPU介入。
4. 缺点:使用固定内存,将失去虚拟内存的所有功能;系统将更快的耗尽内存。
5. 建议:cudaMemcpy()函数调用中的源内存或者目标内存是Host Memory,才使用固定内存,并且在不再需要使用它们时立即释放。
6. 形式:通过cudaHostAlloc()函数来分配;通过cudaFreeHost()释放。
7. 只能以异步方式对固定内存进行复制操作。


Atomicity:

1. 概念:如果操作的执行过程不能分解为更小的部分,我们将满足这种条件限制的操作称为原子操作。
2. 形式:函数调用,如atomicAdd(addr,y)将生成一个原子的操作序列,这个操作序列包括读取地址addr处的值,将y增加到这个值,以及将结果保存回地址addr。


线程同步:

1. 同步方法__syncthreads(),这个函数的调用,将确保线程块中的每个线程都执行完__syscthreads()前面的语句后,才会执行下一条语句。


Performance Evaluation

1. 用途:测GPU在某个任务上花费的时间。CUDA中的事件本质上是一个GPU时间戳。由于事件是直接在GPU上实现的。因此不适用于对同时包含设备代码和主机代码的混合代码设计
2. 形式:首先创建一个事件,然后记录事件,再计算两个事件之差,最后销毁事件。

E.g.

cudaEvent_t start, stop;
cudaEventCreate( &start );
cudaEventCreate( &stop );
cudaEventRecord( start, 0 );
//do something
cudaEventRecord( stop, 0 );
float   elapsedTime;
cudaEventElapsedTime( &elapsedTime,start, stop );
cudaEventDestroy( start );
cudaEventDestroy( stop );


Stream:

1. 并行/并发:并发重点在于一个极短时间段内运行多个不同的任务;并行重点在于同时运行同一个任务。

2. 任务并行性:是指并行执行两个或多个不同的任务,而不是在大量数据上执行同一个任务。
3. 概念:CUDA流表示一个GPU操作队列,并且该队列中的操作将以指定的顺序执行。我们可以在流中添加一些操作,如核函数启动,内存复制以及事件的启动和结束等。这些操作的添加到流的顺序也是它们的执行顺序。可以将每个流视为GPU上的一个任务,并且这些任务可以并行执行。
4. 硬件前提:必须是支持设备重叠功能的GPU。支持设备重叠功能,即在执行一个核函数的同时,还能在设备与主机之间执行复制操作。
5. 声明与创建:声明cudaStream_t stream;创建cudaSteamCreate(&stream)
6. cudaMemcpyAsync():前面在cudaMemcpy()中提到过,这是一个以异步方式执行的函数。在调用cudaMemcpyAsync()时,只是放置一个请求,表示在流中执行一次内存复制操作,这个流是通过参数stream来指定的。当函数返回时,我们无法确保复制操作是否已经启动,更无法保证它是否已经结束。我们能够得到的保证是,复制操作肯定会当下一个被放入流中的操作之前执行。传递给此函数的主机内存指针必须是通过cudaHostAlloc()分配好的内存。(流中要求固定内存
7. 流同步:通过cudaStreamSynchronize()来协调。
8. 流销毁:在退出应用程序之前,需要销毁对GPU操作进行排队的流,调用cudaStreamDestroy()。
9. 针对多个流:
9.1. 对流进行同步操作。
9.2. 将操作放入流的队列时,应采用宽度优先方式,而非深度优先的方式,换句话说,不是首先添加第0个流的所有操作,再依次添加后面的第1,2,…个流。而是交替进行添加,比如将a的复制操作添加到第0个流中,接着把a的复制操作添加到第1个流中,再继续其他的类似交替添加的行为。
9.3. 操作放入流中的队列中的顺序影响到CUDA驱动程序调度这些操作和流以及执行的方式


CUDA ToolKit Documentation:

C/C++ Language Support


你可能感兴趣的:(caffe,CUDA,GPU,CNN)