参考cuda toolkit documentation
利用多核处理器如GPU的编程面临的问题是:如何透明地利用日益增长的核数。
cuda提供了一个简单的编程模型,而又能解决上述问题的方法。
核心分为三个关键抽象:
保留了线程协作的表达性,但是提供了自动扩展的能力。
线程block可以放到任何SM上运行。
图1. 自动扩展性
block间执行的隔离性使得 每个block并不在乎在哪个SM上运行,所以当硬件SM数量比较多时,可以同时运行多个block,当SM数量较小时,block之间可以排队等待在一个SM上运行。自动扩展性的由来。
2.1 kernel
2.2 线程层级结构
2.3 memory hierarchy
2.4 异构编程
分离主机代码和设备代码。
将设备代码编译成汇编格式(PTX code)或二进制格式(cubin object)。
修改主机代码,将<<<...>>>
语法替换成必要的CUDA runtime function calls。来启动 PTX格式代码或者cubin object。
剩下的C++代码要么交给其他要么交给nvcc去编译。
在cudart库中实现,要么通过cudart.lib
、libcudart.a
或者cudart.dll
或libcudart.so
。
连接到同一个cuda runtime instance的组件之间传递cuda runtime symbol的地址才是安全的。
cuda runtime没有显式的初始化函数,当调用第一个runtime函数时才会初始化一个runtime instance。
runtime为系统中每个设备创建一个上下文(context)。
以上操作是透明的,如果需要,可以通过driver API获得。
当线程调用cudaDeviceReset()
时,它将销毁它current work on的primary context。
CUDA interface使用global state:
Context:(具体看J.1节)
参考stackoverflow问答
设备内存可以以_线性内存_和_CUDA 数组_。
CUDA 数组
cuda数组是为纹理拉取优化的不公开数据结构。
线性内存
分配在单一的统一地址空间内。可以用指针相互引用。
地址空间的大小取决于主机结构和GPU计算能力。
x86_64 (AMD64) | POWER (ppc64le) | ARM64 | |
---|---|---|---|
up to compute capability 5.3 (Maxwell) | 40bit | 40bit | 40bit |
compute capability 6.0 (Pascal) or newer | up to 47bit | up to 49bit | up to 48bit |
shared memory比global memory更快。
以下的任务可以相互间并发进行:
从主机的角度来看,以下操作是异步的:
Async
结尾的内存操作函数
- 设置CUDA_LAUNCH_BLOCKING变量可以使设备一时间只运行一个kernel,用于debug。
- kernel启动在有分析器时是同步的,除非支持异步分析器。
Async
函数可能也会因为涉及not page-locked内存而同步起来。
Async
函数使用。cudaStream_t stream[2];
for(int i =0 ; i < 2; i++){
cudaStreamCreate(&stream[i]);
}
// destruction
for(int i =0;i< 2; i++){
cudaStreamDestroy(stream[i]);
}
// cudaStreamDestroy会如此工作:立刻返回成功
// 如果还有工作在这个stream上做,会等它们做完自动返回
没有指定stream、或者设置stream参数为0,会被认为是默认stream。
--default-stream legacy
会让主机所有线程共享一个默认stream,NULL Stream。
--default-stream
flag时的默认情况--default-stream per-thread
,default stream是一个普通stream,并且每个线程都有自己的default stream。null stream上的命令这样调度(设备角度上的同步)参考nvidia default_stream:
有许多显式streams间同步的方法。
cudaDeviceSynchronize()
等待所有主机线程的所有stream的前置任务都完成了。
cudaStreamSynchronize()
将stream作为参数,等待,直到给定stream上所有之前的命令都执行完了。
cudaStreamWaitEvent()
将stream和event作为参数,event之后的调用都被阻塞了。
cudaStreamQuery()
让应用可以知道一个stream中的明林是否都已经执行完了。
两个stream中的指令不可执行,当他们中某个执行在做下面的事:
支持kernel并发执行的设备上,任何需要依赖检查(判定一个streamed kernel是否完成)的操作,
需要依赖检查的操作包括:
cudaStreamQuery()
因此应用如果想要提升kernel并发的性能,需要:
cudaLaunchHostFunc()
压入stream中,当它之前的所有命令都执行完了(kernel运行完、内存拷贝完等),它就会被调用。cudaLaunchHostFunc()
执行完后才能运行。可以使用cudaStreamCreateWithPriority()
来创建带有优先级的stream。
通过cudaDeviceGetStreamPriorityRange()
可以获取优先级。
高优先级的stream会提前工作。
NVIDIA的GPU架构是建立在可扩展的SM组上。当host上的程序调用了一个kernel,blocks of the grid会被枚举,并且分发到不同的SM上(如果SM能支撑起运算)。
以上,
SM运行一个kernel时,可同时reside、process的block数量和warp数量由下列因素决定:
kernel使用的寄存器数量、共享内存使用量(独享SM时理想值)。
实际上SM可用的寄存器数量以及共享内存数量(现实SM的限制)。
最后
SM对reside的warps和block数量限制。
计算能力(compute capability)由这些原因构成:SM对reside的warps、blocks数量的限制,以及寄存器数量、共享内存数量等。
为一个block分配的寄存器数量、和共享内存数量,在CUDA Occupancy Calculator中可以查看。
变量空间指示符指明了变量在设备上的位置。
__device__ __shared__ __constant__
描述的,一般都在寄存器上。指明一个变量在设备上。
cudaGetSymbolAddress()、cudaGetSymbolSize()、cudaMemcpyToSymbol()、cudaMemcpyFromSymbol()
访问。选择性地与__device__
合用,描述这样的变量:
cudaGetSymbolAddress()、cudaGetSymbolSize()、cudaMemcpyToSymbol()、cudaMemcpyFromSymbol()
访问。选择性地与__device__
合用,描述这样的变量:
当在shared memory中声明一个变量作为external array例如:
extern __shared__ float shared[];
// 数组的大小在运行时确定。
// 所有以这种方式声明的,都从内存中相同地址空间开始。
// 所以这个array中展开的变量都必须通过偏移量显式管理。
原子函数执行一个read-modify-write
原子操作在32-bit或64-bit的字,在global或shared内存上。
只有一个线程完成了原子操作,才能被另一个线程访问。
系统级原子性:当前所有线程包括CPUs和GPUs上的线程,都原子性。
atomicAdd_system
。设备级别原子性:当前设备上所有cuda线程原子性。
atomicAdd
block级原子性:当前block内部线程原子性。
atomicAdd_block
。一下场景,启动一个kernel会失效:
Dg
和Db
超过设备能接受的范围。Ns
大于设备能分配的最大share memory。这块附表是对CUDA Runtime提出的概念的补充。
driver API都在cuda(libcuda.so)
动态链接库中实现
cu
开头都是基于handle,不可避免的API:
Obj如下:
CUDA Driver API中可用的Obj
Obj | Handle | 描述 |
---|---|---|
Device | CUdevice | cuda设备 |
Context | CUcontext | CUDA context |
Module | CUmodule | 类似于动态链接库一样的东西 |
Function | CUfunction | kernel |
Heap memory | CUdeviceptr | 指向device memory的指针 |
CUDA array | CUarray | 设备上不透明的一维或二维数据。 |
… | ||
Stream | CUstream | 描述一个Stream的Obj |
Event | CUevent | 描述一个Event的Obj |
cuInit()
初始化,在任何driver API被调用前。
CUDA context类似一个CPU进程。
所有driver API中的资源以及动作都被包括在一个context内。
一个host thread只能有一个device context current at a time。
cuCtxCreate()
创建时,它就变成当前调用线程的current context。CUDA_ERROR_INVALID_CONTEXT
。cuCtxCreate()
会将新context插入栈顶。cuCtxPopCurrent
将context从host thread中detach出来,变成浮流context。如果stack下有context,那么它将成为Current context。每个context都会记录使用数。cuCtxCreate()
创建一个count为1的context,cuCtxAttach()
增加count数,cuCtxDetach()
减少count数。
CUresult cuLaunchKernel(CUfunction f, unsigned int gridDimX, unsigned int gridDimY, unsigned int gridDimZ, unsigned int blockDimX, unsigned int blockDimY, unsigned int blockDimZ, unsigned int sharedMemBytes, CUstream hStream, void ** kernelParams, void ** extra);
cuLaunchKernel
启动一个kernel with 给定的执行配置。参数对齐:
vector type
的对齐要求有表。__alignof()
获取。
double和long long
设置对齐为两个字,而主机编译器对齐为一个字。cuCtxGetCurrent()
可以获取。