鉴于自己的毕设需要使用GPU CUDA这项技术,想找一本入门的教材,选择了Jason Sanders等所著的书《CUDA By Example an Introduction to General Purpose GPU Programming》。这本书作为入门教材,写的很不错。自己觉得从理解与记忆的角度的出发,书中很多内容都可以被省略掉,于是就有了这篇博文。此博文记录与总结此书的笔记和理解。注意本文并没有按照书中章节的顺序来写。书中第8章图像互操作性和第11章多GPU系统上的CUDA C,这两章没有看。等有时间了再看吧,赶紧码字。
CUDA是什么
        CUDA,Compute Unified Device Architecture的简称,是由NVIDIA公司创立的基于他们公司生产的图形处理器GPUs(Graphics Processing Units,可以通俗的理解为显卡)的一个并行计算平台和编程模型。
        通过CUDA,GPUs可以很方便地被用来进行通用计算(有点像在CPU中进行的数值计算等等)。在没有CUDA之前,GPUs一般只用来进行图形渲染(如通过OpenGL,DirectX)。
        开发人员可以通过调用CUDA的API,来进行并行编程,达到高性能计算目的。NVIDIA公司为了吸引更多的开发人员,对CUDA进行了编程语言扩展,如CUDA C/C++,CUDA Fortran语言。注意CUDA C/C++可以看作一个新的编程语言,因为NVIDIA配置了相应的编译器nvcc,CUDA Fortran一样。更多信息可以参考文献。
64位Ubuntu12.04安装CUDA5.5
        具体步骤请点击此处http://bookc.github.io/2014/05/08/my-summery-the-book-cuda-by-example-an-introduction-to-general-purpose-gpu-programming/
[b]对CUDA C的个人懵懂感觉[/b]
        如果粗暴的认为C语言工作的对象是CPU和内存条(接下来,称为主机内存),那么CUDA C工作的的对象就是GPU及GPU上的内存(接下来,称为设备内存),且充分利用了GPU多核的优势及降低了并行编程的难度。一般通过C语言把数据从外界读入,再分配数据,给CUDA C,以便在GPU上计算,然后再把计算结果返回给C语言,以便进一步工作,如进一步处理及显示,或重复此过程。
 主要概念与名称
主机
        将CPU及系统的内存(内存条)称为主机。
设备
        将GPU及GPU本身的显示内存称为设备。
线程(Thread)
        一般通过GPU的一个核进行处理。(可以表示成一维,二维,三维,具体下面再细说)。
线程块(Block)
        1. 由多个线程组成(可以表示成一维,二维,三维,具体下面再细说)。
        2. 各block是并行执行的,block间无法通信,也没有执行顺序。
        3. 注意线程块的数量限制为不超过65535(硬件限制)。
线程格(Grid)
        由多个线程块组成(可以表示成一维,二维,三维,具体下面再细说)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
 * @file_name HelloWorld.cu  后缀名称.cu
 */
 
#include <stdio.h>
#include <cuda_runtime.h>  //头文件
 
//核函数声明,前面的关键字__global__
__global__ void kernel( void ) {
}
 
int main( void ) {
    //核函数的调用,注意<<<1,1>>>,第一个1,代表线程格里只有一个线程块;第二个1,代表一个线程块里只有一个线程。
    kernel<<<1,1>>>();
    printf"Hello, World!\n" );
    return 0;
}

dim3结构类型
        1. dim3是基亍uint3定义的矢量类型,相当亍由3个unsigned int型组成的结构体。uint3类型有三个数据成员unsigned int x; unsigned int y; unsigned int z;
        2. 可使用亍一维、二维或三维的索引来标识线程,构成一维、二维或三维线程块。
        3. dim3结构类型变量用在核函数调用的<<<,>>>中。
        4. 相关的几个内置变量
        4.1. threadIdx,顾名思义获取线程thread的ID索引;如果线程是一维的那么就取threadIdx.x,二维的还可以多取到一个值threadIdx.y,以此类推到三维threadIdx.z。
        4.2. blockIdx,线程块的ID索引;同样有blockIdx.x,blockIdx.y,blockIdx.z。
        4.3. blockDim,线程块的维度,同样有blockDim.x,blockDim.y,blockDim.z。
        4.4. gridDim,线程格的维度,同样有gridDim.x,gridDim.y,gridDim.z。
        5. 对于一维的block,线程的threadID=threadIdx.x。
        6. 对于大小为(blockDim.x, blockDim.y)的 二维 block,线程的threadID=threadIdx.x+threadIdx.y*blockDim.x。
        7. 对于大小为(blockDim.x, blockDim.y, blockDim.z)的 三维 block,线程的threadID=threadIdx.x+threadIdx.y*blockDim.x+threadIdx.z*blockDim.x*blockDim.y。
        8. 对于计算线程索引偏移增量为已启动线程的总数。如stride = blockDim.x * gridDim.x; threadId += stride。
函数修饰符
        1. __global__,表明被修饰的函数在设备上执行,但在主机上调用。
        2. __device__,表明被修饰的函数在设备上执行,但只能在其他__device__函数或者__global__函数中调用。
常用的GPU内存函数
cudaMalloc()
        1. 函数原型: cudaError_t cudaMalloc (void **devPtr, size_t size)。
        2. 函数用处:与C语言中的malloc函数一样,只是此函数在GPU的内存你分配内存。
        3. 注意事项:
        3.1. 可以将cudaMalloc()分配的指针传递给在设备上执行的函数;
        3.2. 可以在设备代码中使用cudaMalloc()分配的指针进行设备内存读写操作;
        3.3. 可以将cudaMalloc()分配的指针传递给在主机上执行的函数;
        3.4. 不可以在主机代码中使用cudaMalloc()分配的指针进行主机内存读写操作(即不能进行解引用)。
cudaMemcpy()
        1. 函数原型:cudaError_t cudaMemcpy (void *dst, const void *src, size_t count, cudaMemcpyKind kind)。
        2. 函数作用:与c语言中的memcpy函数一样,只是此函数可以在主机内存和GPU内存之间互相拷贝数据。
        3. 函数参数:cudaMemcpyKind kind表示数据拷贝方向,如果kind赋值为cudaMemcpyDeviceToHost表示数据从设备内存拷贝到主机内存。
        4. 与C中的memcpy()一样,以同步方式执行,即当函数返回时,复制操作就已经完成了,并且在输出缓冲区中包含了复制进去的内容。
        5. 相应的有个异步方式执行的函数cudaMemcpyAsync(),这个函数详解请看下面的流一节有关内容。
cudaFree()
        1. 函数原型:cudaError_t cudaFree ( void* devPtr )。
        2. 函数作用:与c语言中的free()函数一样,只是此函数释放的是cudaMalloc()分配的内存。
        下面实例用于解释上面三个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <cuda_runtime.h>
__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内存分类
全局内存
        通俗意义上的设备内存。
共享内存
        1. 位置:设备内存。
        2. 形式:关键字__shared__添加到变量声明中。如__shared__ float cache[10]。
        3. 目的:对于GPU上启动的每个线程块,CUDA C编译器都将创建该共享变量的一个副本。线程块中的每个线程都共享这块内存,但线程却无法看到也不能修改其他线程块的变量副本。这样使得一个线程块中的多个线程能够在计算上通信和协作。
常量内存
        1. 位置:设备内存
        2. 形式:关键字__constant__添加到变量声明中。如__constant__ float s[10];。
        3. 目的:为了提升性能。常量内存采取了不同于标准全局内存的处理方式。在某些情况下,用常量内存替换全局内存能有效地减少内存带宽。
        4. 特点:常量内存用于保存在核函数执行期间不会发生变化的数据。变量的访问限制为只读。NVIDIA硬件提供了64KB的常量内存。不再需要cudaMalloc()或者cudaFree(),而是在编译时,静态地分配空间。
        5. 要求:当我们需要拷贝数据到常量内存中应该使用cudaMemcpyToSymbol(),而cudaMemcpy()会复制到全局内存。
        6. 性能提升的原因:
        6.1. 对常量内存的单次读操作可以广播到其他的“邻近”线程。这将节约15次读取操作。(为什么是15,因为“邻近”指半个线程束,一个线程束包含32个线程的集合。)
        6.2. 常量内存的数据将缓存起来,因此对相同地址的连续读操作将不会产生额外的内存通信量。
纹理内存
        1. 位置:设备内存
        2. 目的:能够减少对内存的请求并提供高效的内存带宽。是专门为那些在内存访问模式中存在大量空间局部性的图形应用程序设计,意味着一个线程读取的位置可能与邻近线程读取的位置“非常接近”。如下图:

        3. 纹理变量(引用)必须声明为文件作用域内的全局变量。
        4. 形式:分为一维纹理内存 和 二维纹理内存。
        4.1. 一维纹理内存
        4.1.1. 用texture<类型>类型声明,如texture<float> texIn。
        4.1.2. 通过cudaBindTexture()绑定到纹理内存中。
        4.1.3. 通过tex1Dfetch()来读取纹理内存中的数据。
        4.1.4. 通过cudaUnbindTexture()取消绑定纹理内存。
        4.2. 二维纹理内存
        4.2.1. 用texture<类型,数字>类型声明,如texture<float,2> texIn。
        4.2.2. 通过cudaBindTexture2D()绑定到纹理内存中。
        4.2.3. 通过tex2D()来读取纹理内存中的数据。
        4.2.4. 通过cudaUnbindTexture()取消绑定纹理内存。
固定内存
        1. 位置:主机内存。
        2. 概念:也称为页锁定内存或者不可分页内存,操作系统将不会对这块内存分页并交换到磁盘上,从而确保了该内存始终驻留在物理内存中。因此操作系统能够安全地使某个应用程序访问该内存的物理地址,因为这块内存将不会破坏或者重新定位。
        3. 目的:提高访问速度。由于GPU知道主机内存的物理地址,因此可以通过“直接内存访问DMA(Direct Memory Access)技术来在GPU和主机之间复制数据。由于DMA在执行复制时无需CPU介入。因此DMA复制过程中使用固定内存是非常重要的。
        4. 缺点:使用固定内存,将失去虚拟内存的所有功能;系统将更快的耗尽内存。
        5. 建议:对cudaMemcpy()函数调用中的源内存或者目标内存,才使用固定内存,并且在不再需要使用它们时立即释放。
        6. 形式:通过cudaHostAlloc()函数来分配;通过cudaFreeHost()释放。
        7. 只能以异步方式对固定内存进行复制操作。
原子性
        1. 概念:如果操作的执行过程不能分解为更小的部分,我们将满足这种条件限制的操作称为原子操作。
        2. 形式:函数调用,如atomicAdd(addr,y)将生成一个原子的操作序列,这个操作序列包括读取地址addr处的值,将y增加到这个值,以及将结果保存回地址addr。
常用线程操作函数
        1. 同步方法__syncthreads(),这个函数的调用,将确保线程块中的每个线程都执行完__syscthreads()前面的语句后,才会执行下一条语句。
使用事件来测量性能
        1. 用途:为了测量GPU在某个任务上花费的时间。CUDA中的事件本质上是一个GPU时间戳。由于事件是直接在GPU上实现的。因此不适用于对同时包含设备代码和主机代码的混合代码设计。
        2. 形式:首先创建一个事件,然后记录事件,再计算两个事件之差,最后销毁事件。如:

1
2
3
4
5
6
7
8
9
10
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 );


        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驱动程序调度这些操作和流以及执行的方式。
技巧
        1. 当线程块的数量为GPU中处理数量的2倍时,将达到最优性能。
        2. 核函数执行的第一个计算就是计算输入数据的偏移。每个线程的起始偏移都是0到线程数量减1之间的某个值。然后,对偏移的增量为已启动线程的总数。
实例程序
感兴趣的读者可以下载本书附带的示例代码点击此处下载https://developer.nvidia.com/sites/default/files/akamai/cuda/files/cuda_by_example.zip

转载地址:https://www.cnblogs.com/welhzh/p/8316989.html

转载一篇介绍CUDA的更多相关文章

  1. 关注 硬件 发展, 转载一篇介绍 VHDL 的文章

    <VHDL学习笔记> https://www.eefocus.com/hrbeulvcaho/blog/12-11/289109_978e2.html VHDL 和  “可编程逻辑阵列”  ...

  2. 上一篇括号配对让人联想起catalan数,顺便转载一篇归纳的还不错的文章

    转载请注明来自souldak,微博:@evagle 怎么样才是合法的组合? 只要每一时刻保证左括号的数目>=右括号的数目即可. 直接递归就行,每次递归加一个括号,左括号只要还有就能加,右括号要保 ...

  3. 国内首篇介绍JanOS物联网操作系统的文章 - 如何把你的手机主板打造成物联网平台

    天地会珠海分舵注:如无意外,您现在正在看的将是国内首篇且是唯一一篇介绍炙手可热的物联网的操作系统JanOS的文章!不信你去百度!希望大家能喜欢.但本文只是引言,更多信息请还是访问JanOS的官网:ht ...

  4. 第三篇:CUDA 标准编程模式

    前言 本文将介绍 CUDA 编程的基本模式,所有 CUDA 程序都基于此模式编写,即使是调用库,库的底层也是这个模式实现的. 模式描述 1. 定义需要在 device 端执行的核函数.( 函数声明前加 ...

  5. Java并发包下锁学习第一篇:介绍及学习安排

    Java并发包下锁学习第一篇:介绍及学习安排 在Java并发编程中,实现锁的方式有两种,分别是:可以使用同步锁(synchronized关键字的锁),还有lock接口下的锁.从今天起,凯哥将带领大家一 ...

  6. spring cloud系列教程第一篇-介绍

    spring cloud系列教程第一篇-介绍 前言: 现在Java招聘中最常见的是会微服务开发,微服务已经在国内火了几年了,而且也成了趋势了.那么,微服务只是指spring boot吗?当然不是了,微 ...

  7. 老猿学5G扫盲贴:推荐三篇介绍HTTP2协议相关的文章

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 5G中的服务化接口调用都是基于HTTP2协议的,老 ...

  8. [转载] 十五分钟介绍 Redis数据结构

    转载自http://blog.nosqlfan.com/html/3202.html?ref=rediszt Redis是一种面向“键/值”对类型数据的分布式NoSQL数据库系统,特点是高性能,持久存 ...

  9. 2、转载一篇,浅析人脸检测之Haar分类器方法

    转载地址http://www.cnblogs.com/ello/archive/2012/04/28/2475419.html 浅析人脸检测之Haar分类器方法  [补充] 这是我时隔差不多两年后, ...

随机推荐

  1. anoconda 神经网络 相关操作

    1. conda 相关操作 建立新环境:conda crearte -n NewName python=版本(3.6) 激活环境:conda activate NewName 关闭环境:conda d ...

  2. HyperLedger/Fabric SDK使用Docker容器镜像快速部署上线

    HyperLedger/Fabric SDK Docker Image 该项目在github上的地址是:https://github.com/aberic/fabric-sdk-container ( ...

  3. 桌面输入法评测报告 之 搜狗拼音输入法vs必应拼音输入法

    输入法是电脑用户不可或缺的软件,它几乎无时无刻不陪伴在使用者的身旁.一个优秀的输入法,应该满足客户对使用体验以及效率的需求.我们小队的任务便是对当今最为常用的两种输入法: 搜狗拼音输入法和必应拼音输入 ...

  4. Linux实验报告

    第一次链接: http://www.cnblogs.com/L1nke/p/4966820.html 第二次链接: http://www.cnblogs.com/L1nke/p/4992758.htm ...

  5. 第一Sprint阶段对各组提出的意见

    组号  组名 意见 1 理财猫 1.界面深色的部分,字体应调成亮色,可以适当美化字体. 2.希望能够自动记录花销,而不是只能手动“记一笔”. 3.功能略少,比如可以添加“收入详情”.“支出详情”. 2 ...

  6. hibernate 创建工厂类

    package cn.hibernate; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; / ...

  7. ELK之消息队列选择redis_kafka_rabbitmq

    前言描述 生产初级,Service服务较少,访问量较少,随着业务量的不断增加,日志量成倍增长,然后就遇到了消息队列redis被充爆,不能满足应用的情况.针对此情况,我们来分析下可用的消息多列. 官方推 ...

  8. Spring IOC AOP的原理 如果让你自己设计IOC,AOP如何处理(百度)

    百度的面试官问,如果让你自己设计一个IOC,和AOP,如何设计, 我把IOC的过程答出来了,但是明显不对, (1) IOC 利用了反射,自己有个id,classtype,hashmap,所有的功能都在 ...

  9. vue中的minix

    minix 是个什么东西, 就是混合,把你混合给我 浅显表述就是 你说 : ‘我叫李四’, 我说 : ‘我叫张三’, 然后把你 混合给我, 就成了 我说 : ‘我叫张三我叫李四’, 所有解说都在例子里 ...

  10. Tomcat源码解析-整体流程介绍

    一.架构 下面谈谈我对Tomcat架构的理解 总体架构: 1.面向组件架构 2.基于JMX 3.事件侦听 1)面向组件架构 tomcat代码看似很庞大,但从结构上看却很清晰和简单,它主要由一堆组件组成 ...