GPU加速类似于批处理或工厂加工,指针指定的向量数据并行经过同样的处理过后输出到显存中。因此GPU加速的范围是比较局限的,但在处理大数据方面优势巨大。
由于平常使用的程序都是在CPU上运行,数据都是在系统内存上操作。因此, 在使用GPU参与程序计算时,需要将数据从内存传输到显存上,然后才可以进行计算,并且计算结果存储在显存中。
因此,GPU程序时间开销分为两部分:1.显存与内存的调用和数据在内存与显存之间的传输; 2.程序复杂度和并行程度。
首先检查一下设备的型号和性能。本机采用的是GTX1060MaxQ显卡(Pascal架构),具体数据如下:
一.各部分数据详解:
cudaMemcpy
函数就是把CPU上的数据传输到GPU的全局内存上。
二 . GPU内存详解
GPU的内存中可读写的有:寄存器(registers)、本地内存、共享内存(shared memory)和全局内存(global memory),只读的有:常量内存(constant memory)和纹理内存(texture memory)。以下是对几种内存的介绍:
全局、常量和纹理内存空间在同一应用程序的内核发射中持久存在。
CUDA驱动程序API和CUDA运行时是CUDA的两个编程接口。
CUDA软件环境的主机运行时组件只能由宿主函数来使用。它提供了处理以下问题的功能:
设备管理
上下文管理
内存管理
代码模块管理
执行控制
纹理参考管理
与OpenGL和Direct3D的互操作性
与较低级别的CUDA驱动程序API相比,CUDA运行时通过提供隐式初始化、上下文管理和设备代码模块管理,极大地简化了设备管理。nvcc生成的c/c++主机代码使用CUDA运行时,因此链接到此代码的应用程序将依赖于CUDA运行时;类似地,任何使用cuBLAS、cuFFT和其他CUDA工具箱库的代码也将依赖于CUDA运行时,这些库是由这些库内部使用的。
组成CUDA Runtime API的功能在CUDA工具包参考手册中解释。
CUDA运行时处理内核加载和设置内核参数,并在内核启动之前启动配置。隐式驱动程序版本检查、代码初始化、CUDA上下文管理、CUDA模块管理(cubin到函数映射)、内核配置和参数传递都是由CUDA运行时执行的。
①函数执行空间说明符
函数执行空间说明符表示一个函数是否在主机上或设备上执行,以及它是否可以从主机或设备调用。
__device__声明了一个函数:在设备上执行,只能从设备上调用。
__global__声明了一个函数:在设备上执行,从主机端调用。(3.2计算能力以上的设备,设备端可以调用??)
__host__ 声明了一个函数:在主机上执行,只能从主机端调用。在任何一种情况下,该函数仅为主机编译。
__device__ __host__声明了一个函数:该功能是为主机和设备编译的。
__global__ 和 __host__不可以同时声明同一个函数。
注意:但是CUDA运行时,CUDA驱动或者其他CUDA工具箱库都有相同功能的设备端函数和主机端函数,并且两者之间功能相同但是计算速度不同。
②数据和变量
Dim3是一个基于uint3的整数向量类型,用于指定维度。在定义dim3的变量时,未指定的任何组件都被初始化为1。
内置的变量
内建变量指定网格和块维度以及块和线程索引。它们只在在设备上执行的函数中有效。
gridDim: dim3类型 网格的尺寸。
blockIdx:uint3型 网格中的块索引。
blockDim:dim3类型 块的大小。
threadIdx: uint3型 区块内的线程索引。
warpSize: int型的,线程束大小
1.使用CPU计时器
在使用CPU计时器时,重要的是要记住许多CUDA API函数是异步的;也就是说,在完成工作之前,它们会将控制权返回给调用CPU线程。所有的内核发射都是异步的,就像在名称上使用Async后缀的内存复制函数一样。因此,为了准确地测量一个特定呼叫或CUDA调用序列的运行时间,有两个方法。
1.在启动和停止CPU计时器之前立即调用cudadevicesyn计时器()来同步CPU线程。cudadevicesyn计时()阻塞呼叫CPU线程,直到线程之前发出的所有CUDA调用都完成。
2.将CPU线程与GPU上的特定流或事件同步,但这些同步功能不适合在流以外的流中计时代码。cudastream同步()阻塞了CPU线程,直到先前发出的所有CUDA调用都已经完成。cudaevent同步()块直到特定流中的某一事件被GPU记录下来。因为驱动程序可能会在其他非默认流中插入CUDA调用的执行,所以在其他流中调用可能包含在时间内。
2.内存优化
内存优化是性能的最重要的领域。我们的目标是通过最大化带宽来最大化硬件的使用。使用尽可能多的快速内存和尽可能少的慢访问内存来提供带宽是最好的。本章讨论了主机和设备上的各种内存,以及如何最好地设置数据项来有效地使用内存。
设备内存和GPU之间的峰值理论带宽要高得多,因此,为了获得最佳的整体应用程序性能,重要的是最小化主机和设备之间的数据传输。注意:最小化主机和设备之间的数据传输,即使这意味着在设备上运行一些内核,而与在主机CPU上运行相比,它不会显示性能提升。
中间数据结构应该在设备内存中创建,由设备操作,并且在没有被主机映射或复制到主机内存的情况下被销毁。
3.内存选择
①固定内存
页面锁定或固定的内存传输在主机和设备之间达到最高的带宽。固定内存是在运行时API中使用cudaHostAlloc()函数分配的。带宽测试CUDA示例展示了如何使用这些功能以及如何测量内存传输性能。对于已经预先分配的系统内存区域,cudaHostRegister()可以在不需要分配一个单独的缓冲区并将数据复制到它的情况下,在动态地固定内存。
②零复制内存
零拷贝是CUDA工具箱的第2.2版中添加的一个特性。它使GPU线程能够直接访问主机内存。需要映射固定的(不可分页的)内存。综合gpu(即:GPUs与CUDA设备属性结构的集成字段设置为1),映射固定内存始终是一种性能提升,因为它避免了多余的副本,因为集成的GPU和CPU内存在物理上是相同的。零拷贝可以用来代替流,因为内核发起的数据传输自动重叠内核执行,而不需要设置和确定最佳流的开销(这句话没搞懂)。
cudaGetDeviceProperties()返回的结构的canMapHostMemory字段用来检查设备是否支持将主机内存映射到设备的地址空间。
cudaDeviceMapHost来调用cudaSetDeviceFlags()来启用页面锁定的内存映射。
在设置设备或发出需要状态的CUDA调用之前必须调用cudaSetDeviceFlags()。
页面锁定的映射主机内存是使用cudaHostAlloc()分配的,
而指向映射设备地址空间的指针是通过函数cudaHostGetDevicePointer()获得的。
内核()可以使用指针amap来引用映射的固定主机内存,如果amap指向设备内存中的位置,它就会这样做。
映射固定的主机内存在避免使用CUDA流的同时,可以将cpu-gpu内存传输与计算重叠。
③统一内存
统一虚拟寻址Unified Virtual Addressing(UVA)了,Unified Memory依赖于UVA,但他们不同。UVA为系统中所有内存提供虚拟的单一的虚拟内存地址,不论是设备内存,主机内存或是片上共享内存。它允许cudaMemcpy的使用,不管输入和输出参数在哪。UVA能够使用“Zero-Copy” memory, 一种pinned host memory,设备端能够通过PCI-Express直接获取,不需要memcpy。Zero-Copy提供了一些统一内存的便利性,但性能并不好,因为它总是和PCI-Express的低带宽和高延迟相关的。
4.异步传输
使用cudaMemcpy()的主机和设备之间的数据传输是阻塞传输;也就是说,只有在数据传输完成之后,控制权才会返回给主机线程。cudaMemcpyAsync()函数是cudaMemcpy()的非阻塞变种,在这种变体中,控制权会立即返回给主机线程。与cudaMemcpy()相反,异步传输版本需要固定的主机内存,并且它包含一个额外的参数,一个streamID。一个stream,不同stream中的操作可以是交错的,在某些情况下是重叠的——这是一种可以用来隐藏主机和设备之间数据传输的属性。
异步传输可以通过两种不同的方式实现数据传输的重叠。
①CPU和GPU上的同步计算,在所有支持cuda的设备上,都可以使用异步数据传
输和设备计算来重叠主机计算。例如,重叠的计算和数据传输演示了如何在数据传输到设备时执行例行cpuFunction()的主机计算,并执行使用该设备的内核。
cudaMemcpyAsync(a_d, a_h, size, cudaMemcpyHostToDevice, 0); kernel<<<grid, block>>>(a_d);
cpuFunction();
cudaMemcpyAsync()函数的最后一个参数是stream ID,在本例中使用默认流,流0。内核也使用默认流,直到内存复制完成,它才会开始执行;因此,不需要显式的同步。由于内存拷贝和内核都立即将控制权返回给主机,主机函数cpuFunction()与它们的执行重叠。
②在重叠的计算和数据传输中,内存拷贝和内核执行顺序发生。
在能够并发复制和计算的设备上,可以在设备上重叠内核执行,并在主机和设备之间进行数据传输。一个设备是否具有这种能力是由cudadevice自营结构的asyncEngineCount字段(或者在设备equery CUDA样本的输出中列出)来指示的。在具有这种功能的设备上,重叠再次需要固定的主机内存,此外,数据传输和内核必须使用不同的、非默认的流(带有非零id的stream)。这种重叠需要非默认stream,因为只有在设备(在任何流)上的所有调用都完成之后,才会开始使用默认stream的内存拷贝、内存集函数和内核调用,并且设备(在任何流中)都没有操作,直到它们完成为止。
cudaMemcpy(a_d, a_h, N*sizeof(float), dir);
kernel<<
5.线程同步
同步是并行编程的一个普遍的问题。在CUDA的世界里,有两种方式实现同步:
System-level:等待所有host和device的工作完成
Block-level:等待device中block的所有thread执行到某个点
Stream-level:等待给定流中的所有命令完成
Event-level : 等待事件中的所有命令完成
CUDA API和host代码是异步的,cudaDeviceSynchronize可以用来停住CUP等待CUDA中的操作完成:
block中的thread执行顺序不定,CUDA提供了__device__ void __syncthreads(void);
一个function来同步block中的thread。当该函数被调用,block中的每个thread都会等待所有其他thread执行到某个点来实现同步。
cudastream同步()将溪流作为参数,并等待给定流中的所有命令完成。它可以用来同步主机和特定的流,允许其他流继续在设备上执行。
cudaStreamWaitEvent()将溪流和事件作为参数(参见事件描述的事件),并在调用cudaStreamWaitEvent()之后将所有的命令添加到给定的流中,直到给定事件完成为止。流可以是0,在这种情况下,在调用cudaStreamWaitEvent()之后,所有的命令都会添加到任何流中。
cudaStreamQuery()为应用程序提供了一种方法,以知道流中的所有前面的命令是否已经完成。
6.核函数并发执行
在能够并发内核执行的设备上,流还可以同时执行多个内核,以便更充分地利用该设备的多处理器。一个设备是否具有这种能力是由cudadevice自营结构的concurrent内核字段(或者在设备equery CUDA样本的输出中列出)来指示的。例如:
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
kernel1<<
kernel2<<
CUDA将以下操作作为独立的任务公开,可以并发地操作:
主机上的计算;
设备上的计算;
内存从主机转移到设备;
内存从设备传输到主机;
在给定设备的内存中进行内存传输;
设备之间的内存传输。
7.原子函数
一个原子函数在一个32位或64位的单词上执行一个读-修改-写的原子操作,在全局或共享内存中。例如,atomicAdd()在全球或共享内存中的某个地址读取一个单词,向它添加一个数字,并将结果写回相同的地址。这个操作是原子的,因为它保证在不受其他线程干扰的情况下执行。换句话说,在操作完成之前,没有其他线程可以访问这个地址。
8.规约思想
与算法有关,将串行计算思想优化为并行计算。
多用于点积,累加,比较等同一数组数据相互计算的过程
9.占用
分为两部分,线程占用和内存吞吐量最大化。
一,多处理器创建、管理、调度和执行线程,这些线程有32个并行线程,称为warps。一个线程束由32个线程组成,所以,ThreadsperBlock最好为32的倍数。
二,确保最大化内存吞吐量的下一步是根据设备内存访问中描述的最优内存访问模式,尽可能地组织内存访问。当全局内存带宽较低时,这种优化对于全局内存访问尤其重要,因此非最优全局内存访问对性能有更高的影响。