Create by Jane/Santaizi 03:57:00 3/14/2016
All right reserved.html
速查手册基于 CUDA 7.0 toolkit documentation 并对原文进行了精简.前端
手册专一于CUDA的GPU计算方面,不涉及图形显示.如需完整档请查原文http://docs.nvidia.com/cuda/index.html#axzz42oaojUNj编程
在Host CPU程序中划出的内存区域供多GPU设备共享使用缓存
使用方法:并发
优势:app
缺点:异步
在多GPU设备之间充当共享内存角色.是一个 Unified Virtual Address Space.async
使用方法:ide
默认 page-locked host 内存是以 cacheable 方式分配的.你能够用 Write-Combining 方式分配. Write-Combining 内存释放 host 的L1,L2缓存资源, 在通过PCI总线时提升最多40%的速度.函数
使用方法:
优势:
缺点:
host CPU内存和GPU内存之间的内存地址映射.
host 和GPU有对应的内存指针. 函数返回的是 host 指针, GPU内存指针需用 cudaHostGetDevicePointer() 获取,获取的GPU内存指针能够在 kernel中去使用.
使用方法:
优势:
缺点:
注意:在获取GPU内存指针以前必须使用 cudaSetDeviceFlags(), 传入 flag cudaDeviceMapHost.不然 cudaHostGetDevicePointer() 会致使错误. cudaHostGetDevicePointer() 错误也会在 设备GPU不支持内存映射时产生.
属性查看:
一样注意: Atomic Functions 对映射内存的原子操做对host 和 GPU设备来讲也是非原子的.
CUDA中如下操做是互相独立且并发的:
除了Host(CPU)环境内部的数据传递是顺序同步的以外,一切和GPU有关的数据传递都是独立并发的(异步).
Host 中的并发操做是经过异步函数库方法实现的,并在启动后直接移交控制权回 Host 主线程,且并不保证GPU设备已经计算完相应任务.这个模式相似于 event loop,任务在异步启动后排队等待被处理,而不阻塞主线程.下面几种操做对 host 来讲是异步调用的:
能够设置环境变量 CUDA_LAUNCH_BLOCKING = 1 来禁止 kernel 函数的异步启动. 这个特性只能用来 debug (Notice: Debug Only!).
另外在使用 Visual Profiler Nsight 采集硬件计数器的时候 kernel 的启动也是同步的, 除非 concurrent kernel profiling 选项被开启. 以 Async 后缀的内存拷贝一样在 not page-locked 的 host 内存中是同步的.
设备计算能力超过2.X均可以并发执行 kernel 函数. 在附录表13中可查. 不一样CUDA context中的kernel 不能并发. 使用大量 texture 和 内存的 kernel 也不太可能与其余并发.
属性查看:
一些设备可并发执行 kernel函数和异步GPU内存拷贝操做. Host 内存块必须是 page-locked的. Device内存内部的多个内存拷贝(intra-device)和 kernal 函数甚至能够同时执行.
属性查看:
设备计算能力超过2.X 能够执行并发内存拷贝.Host 内存必须为 page-locked.
属性查看:
应用程序使用 streams 来管理上述全部并发操做.一个 stream 就是一串顺序命令. 不一样 streams 之间是乱序或同步执行的.
使用方法:
下例中建立了2个 stream 并分配了一个 float array 的 page-locked 内存块给 hostPtr
cudaStream_t stream[2]; for (int i = 0; i < 2; ++i) cudaStreamCreate(&stream[i]); float * hostPtr; cudaMallocHost(&hostPtr, 2*size);
每一个 stream 都被指定顺序执行下述操做:
Device -> Host 的内存拷贝
for (int i = 0; i < 2; ++i)
{
cudaMemcpyAsync(inputDevPtr + isize, hostPtr + isize, size, cudaMemcpyHostToDevice, stream[i]);
MyKernel <<<100, 512, 0, stream[i]>>>(outputDevPtr + isize, inputDevPtr + isize, size);
cudaMemcpyAsync(hostPtr + isize, outputDevPtr + isize, size, cudaMemcpyDeviceToHost, stream[i]);
}
释放 streams 使用 cudaStreamDestroy().
for (int i = 0; i < 2; ++i) cudaStreamDestory(stream[i]);
cudaStreamDestory() 等待全部 stream 中的命令执行完毕后再销毁 stream 并返回控制权给 host 主线程,也就是说它是一个阻塞的强制同步函数.
kernel 启动和 host-device 之间的内存拷贝不须要设置特殊 stream 参数(默认设置为 0 ), 他们在stream中顺序执行.
使用方法:
下面列举了几种显式同步各个 streams 的方法. 为了不运算性能下降, 全部同步函数都应在须要时间控制和分离启动与内存拷贝(顺序控制)时使用.
使用方法:
若是碰到如下状况, 两个 stream 中的命令是不能并发执行的:
对于那些支持并发 kernel 执行的设备来讲, 任何操做都须要附加一个检查来查看 streamed kernel launch是否已经完成:
由于操做须要作一个 cudaStreamQuery()检查,因此为了提升性能应遵循下面两个习惯:
两个 stream 上的命令能够根据设备的支持状况进行重叠(并发)执行. 对于3.2.5.5.1 Creation and Destruction 例子
for (int i = 0; i < 2; ++i) { cudaMemcpyAsync(inputDevPtr + i*size, hostPtr + i*size, size, cudaMemcpyHostToDevice, stream[i]); MyKernel <<<100, 512, 0, stream[i]>>>(outputDevPtr + i*size, inputDevPtr + i*size, size); cudaMemcpyAsync(hostPtr + i*size, outputDevPtr + i*size, size, cudaMemcpyDeviceToHost, stream[i]); }
对于 stream[0]、 stream[1] 来讲,2次循环前一次中 stream[0]里的 cudaMemcpyAsync DeviceToHost 和后一次循环中 stream[1]里的 cudaMemcpyAsync HostToDevice 操做能够重叠(并发), 固然这要求设备支持并发数据传输(Concurrent Data Transfer). 可是就上述代码而言,即便设备支持并发Kernel执行(Concurrent Kernel and Kernel Execution),它也不太可能跳过两次内存拷贝过程使 stream[0]和stream[1]的 kernel执行并发,因此是隐式同步(Implicit Synchronization).为了充分利用 并发数据传输(Concurrent Data Transfer)和并发Kernel执行(Concurrent Kernel and Kernel Execution)这两个特性,重写代码以下
for (int i = 0; i < 2; ++i) cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]); for (int i = 0; i < 2; ++i) MyKernel<<<100, 512, 0, stream[i]>>> (outputDevPtr + i * size, inputDevPtr + i * size, size); for (int i = 0; i < 2; ++i) cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);
上述代码即便在设备不支持 kernel并发执行的状况下:
stream[0]的 kernel执行和 stream[1]的 cudaMemcpyAsync HostToDevice内存拷贝能够重叠, stream[0]的 cudaMemcpyAsync DeviceToHost内存操做和 stream[1]的kernel执行也能够重叠.
上述代码在设备支持 kernel并发及 data transfer并发的状况下:
stream[0] 和 stream[1]中 cudaMemcpyAsync HostToDevice/DeviceToHost 并发 ,kernel 执行并发.
两种方法比较之下后一种充分利用了设备的任务重叠并发特性(从一次增长到三次).即便设备不支持,也增长了一次重叠并发(从一次并发增长到两次).
CUDA-runtime 提供了在stream中的函数回调.
使用方法:
下例添加 MyCallback函数回调至每一个 stream DeviceToHost内存拷贝操做以后:
void CUDART_CB MyCallback(cudaStream_t stream, cudaError_t status, void *data){ printf("Inside callback %d\n", (size_t)data); } ... for (size_t i = 0; i < 2; ++i) { cudaMemcpyAsync(devPtrIn[i], hostPtr[i], size, cudaMemcpyHostToDevice, stream[i]); MyKernel<<<100, 512, 0, stream[i]>>>(devPtrOut[i], devPtrIn[i], size); cudaMemcpyAsync(hostPtr[i], devPtrOut[i], size, cudaMemcpyDeviceToHost, stream[i]); cudaStreamAddCallback(stream[i], MyCallback, (void*)i, 0); }
cudaStreamAddCallback 函数最后一个参数为 0 ,是CUDA保留为了未来新功能的加入.
注意: 回调中绝对不能调用CUDA API(直接或间接), 这会致使自我调用的死循环.
设置 stream的优先级.
使用方法:
例子:
// get the range of stream priorities for this device int priority_high, priority_low; cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high); // create streams with highest and lowest available priorities cudaStream_t st_high, st_low; cudaStreamCreateWithPriority(&st_high, cudaStreamNonBlocking, priority_high); cudaStreamCreateWithPriority(&st_low, cudaStreamNonBlocking, priority_low);
events 提供了能够监控设备进程的方法.和回调同样,它在特定的 stream中被触发.
传入参数 stream = 0 表示等待全部 stream 中的命令完成后触发该事件.
例子:
建立:
cudaEvent_t start, stop;
cudaEventCreat(&start);
cudaEventCreat(&stop);
销毁:
cudaEventDestroy(start);
cudaEventDestroy(stop);
下例使用 event 记录时间:
// 添加 start event 至全部 streams中 cudaEventRecord(start, 0); for (int i = 0; i < 2; ++i) { cudaMemcpyAsync(inputDev + i * size, inputHost + i * size, size, cudaMemcpyHostToDevice, stream[i]); MyKernel<<<100, 512, 0, stream[i]>>> (outputDev + i * size, inputDev + i * size, size); cudaMemcpyAsync(outputHost + i * size, outputDev + i * size, size, cudaMemcpyDeviceToHost, stream[i]); } // 在全部命令添加完以后往 streams中添加 end event cudaEventRecord(stop, 0); // 同步等待全部 streams中命令完成后到达 stop event cudaEventSynchronize(stop); float elapsedTime; // 记录 start event 至 stop event的时间消耗 cudaEventElapsedTime(&elapsedTime, start, stop);
当同步函数被调用以后, 直达全部相关命令执行结束后才返回控制权.使用 cudaSetDeviceFlags() 决定在同步结束后 host 线程行为是 yield,block仍是spin.
一个 host 系统能够拥有多个设备Device. 例子中遍历设备并获取他们的属性.
int deviceCount; cudaGetDeviceCount(&deviceCount); int device; for (device = 0; device < deviceCount; ++device) { cudaDeviceProp deviceProp; cudaGetDeviceProperties(&deviceProp, device); printf("Device %d has compute capability %d.%d. \n", device, deviceProp.major, deviceProp.minor); }
一个 Host线程能够在任什么时候候使用 cudaSetDevice() 来指配设备进行运算.并切换全部执行环境.分配内存,kernel launch,streams,events等,都在最近指定的设备GPU上运行. 若是没有指定则当前选择设备号 = 0.
例子:
size_t size = 1024sizeof(float);
cudaSetDevice(0); //切换到设备0
float p0;
cudaMalloc(&p0, size); //在设备0 上分配global内存
MyKernel<<<1000, 128>>>(p0); //在设备0 上执行kernel函数
cudaSetDevice(1); //切换到设备 1
float * p1;
cudaMalloc(&p1, size); //在设备1 上分配global内存
MyKernel<<<1000, 128>>>(p1); //在设备1 上执行kernel函数
在多GPU设备的条件下,耗时的任务能够指派给多个GPU进行运算.这是很好的.(SLI技术是多GPU完成单个任务,与这个不一样)
注意: kernel launch在 stream与当前 device没有关联的状况下会失败.
失败例子:
cudaSetDevice(0); //切换到设备0
cudaStream_t s0;
cudaSreamCreate(&s0); //在当前设备0 中建立 stream s0
MyKernel<<<100,64,0,s0>>>(); //在当前设备0 中的 stream s0 中加入(异步) kernel launch指令
cudaSetDevice(1); //切换到设备1 cudaStream_t s1; cudaSreamCreate(&s1); //在当前设备1 中建立 stream s1 MyKernel<<<100,64,0,s1>>>(); //在当前设备1 中的 stream s1 中加入(异步) kernel launch指令 // 上述代码是正确的 // 下面这行代码会失败 MyKernal<<<100,64,0,s0>>>(); #Error //在当前设备1 中试图往设备0 中的 stream s0加入kernel launch指令
而内存拷贝指令却与当前设备选择无关:
// 下述代码是正确的
cudaSetDevice(0); //切换到设备0
cudaStream_t s0;
cudaSreamCreate(&s0); //在当前设备0 中建立 stream s0
cudaSetDevice(1); //切换到设备1 cudaMemcpyAsync(devMemPtr, hostMemPtr, size, cudaMemcpyHostToDevice, s0); //This is OK
cudaEventRecord() 在 stream与当前 device没有关联的状况下会失败.
cudaEventElapsedTime() 在 stream与当前 device没有关联的状况下会失败.
cudaEventSynchronize() , cudaEventQuery() ,cudaStreamWaitEvent() 与当前设备选择无关
所以 cudaStreamWaitEvent() 能够在多个GPU设备之间作同步.
每一个设备拥有本身的默认 stream (see Default Stream).因此不一样 GPU设备之间的任务执行是独立无序的,你须要本身控制设备间的同步问题.
应用程序若是在 64位处理器上执行的话,计算能力超过2.0的 Tesla系列显卡能够互相引用他们的内存地址(i.e. 一个kernel可使用另外一个设备内存地址中的数据来执行运算) 这个点对点的内存获取特性可使用 cudaDeviceCanAccessPeer() = true检查支持状况.
点对点的内存获取功能必须使用函数 cudaDeviceEnablePeerAccess() 开启.每一个设备能够支持全局最多 8个点的内存连接.
下例为两个设备之间的数据传递:
cudaSetDevice(0);
float p0;
size_t size = 1024sizeof(float);
cudaMalloc(&p0,size);
MyKernel<<<1000,128>>>(p0);
cudaSetDevice(1);
cudaDeviceEnablePeerAccess(0,0); //开启对设备0 的点对点通道
// 在设备0 上launch kernel ,且该kernel使用设备0 中的地址 p0 MyKernel<<<1000,128>>>(p0);
两个设备之间的点对点内存拷贝.
例子:
cudaSetDevice(0); // Set device 0 as current
float* p0;
size_t size = 1024 * sizeof(float);
cudaMalloc(&p0, size); // Allocate memory on device 0
cudaSetDevice(1); // Set device 1 as current
float* p1;
cudaMalloc(&p1, size); // Allocate memory on device 1
cudaSetDevice(0); // Set device 0 as current
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
cudaMemcpyPeer(p1, 1, p0, 0, size); // Copy p0 to p1
MyKernel<<<1000, 128>>>(p1); // Launch kernel on device 1
A copy (in the implicit NULL stream) between the memories of two different devices
部分掠过详细请查阅CUDA7.5 toolkit Documentation
当程序运行在 64位处理器上时, 一个64位的内存地址能够供全部2.0以上设备和host所使用. 全部使用 CUDA API分配的 host 内存和全部 device 内存都在这个虚拟地址范围内.(换句话说64位处理器提供的指针地址范围够大了),咱们称为这个虚拟地址为通用的(unified).
咱们称它为通用虚拟地址是由于它并不表明真实的内存地址,而是一个虚拟地址到真实地址的内存地址映射(真实的内存地址是malloc出来的内存地址),为了编程方便咱们须要多个设备和host统一使用同一个内存地址规范,而通用虚拟地址解决了这个问题.
优势:
缺点:
可使用设备属性 unifiedAddressing = 1查看设备是否使用了通用内存地址.
全部由 host线程建立分配的 Device内存指针或者 event handle 均可以在程序进程中全部的线程使用,但不能跨进程.
若是想要跨进程使用指针和事件,必须使用 InterProcess Communication API.详细可查阅 reference manual. 而且该功能只在64位 Linux系统上受到支持.(部份内容略)
全部 run-time 函数均返回 error code.但对于异步并发(Async)函数来讲,返回错误是不可能的(基于一些缘由).因此必须使用一些 host run-time 函数来获得相关错误.
检查异步错误的惟一方法是使用对应同步函数. 使用 cudaDeviceSynchronize() 函数来同步设备已得到在设备上发生的异步错误.
你也可使用不一样级别的同步函数,好比cudaStreamSynchronize(), cudaStreamWaitEvent(), __syncthreads()等.
通常 run-time函数返回 cudaSuccess做为异常指示标志.
kernel launch并不像其余 run-time函数那样返回错误标识,因此必须使用上述两种方法获取错误. 而且这两个函数必须紧跟 kernel launch函数,来得到 pre-launch errors. 由于全局只有一个Error,而咱们不但愿当中有任何函数引发的 Error 覆盖了它.为了保险起见,在 kernel launch以前也使用 cudaGetLastError()来获取以前的异常并重置为 cudaSuccess.
注意: cudaStreamQuery() 和 cudaEventQuery() 可能返回 cudaErrorNotReady ,它并不被认为是一种异常错误,因此不会被上述方法所捕捉到.
在计算能力超过2.0的设备上可使用 cudaDeviceGetLimit(), cudaDeviceSetLimit() 查询和设置调用栈的大小.
当栈溢出的时候, kernel call会失败并返回一个栈溢出错误.
数据采集自GeForce-GTX760:
cudaLimitStackSize: 1024 bytes cudaLimitPrintfFifoSize: 1048576 bytes cudaLimitMallocHeapSize: 8388608 bytes cudaLimitDevRuntimeSyncDepth: 8388608 cudaLimitDevRuntimePendingLaunchCount: 8388608
CUDA支持一些具备 texturing功能(Tesla系列就没有)的GPU设备使用 texture 和 surface内存. 从texture 或者 surface内存中读取数据比从 global内存中读取有的优点在于如下几点: