在同等价位和power envelope情况下,Graphics Processing Unit(GPU) 比CPU提供了更高的instruction throughput 和 memory bandwidth。其它计算设备如FPGA,也是energy efficient,但是不如GPU的program flexibility。
GPU和CPU的设计目标不同:
GPU擅长高度并行计算,因此,将更多的transistors用于data processing(如浮点计算),而不是data caching和flow control:
GPU可hide memory access latencies with computation, instead of relying on large data caches and complex flow control来avoid long memory access latencies,因为data caches和flow control都是expensive in terms of transistors。
通常,应用中包含了串行部分和并行部分,而系统通常也设计为GPU和CPU的混合体,以实现总体性能最优。具有高度并行性的应用可利用GPU的并行特征实现 比 CPU更优的性能。
2006年11月,NVIDIA®引入CUDA®: A General-Purpose Parallel Computing Platform and Programming Model——可使用C++等语言作为high-level programming language。
可采购相应的显卡,也可选择GPU云服务器。
multicore CPU和manycore GPU的出现意味着主流处理器芯片都是并行系统。软件开发的挑战在于:transparently scale其并行性,以充分利用新增的processor cores。CUDA设计为降低C语言开发者的学习曲线。
GPU core有3个关键抽象:
这些抽象提供了细粒度的data parallelism和thread parallelism,嵌套在粗粒度的data parallelism 和 task parallelism 之内。这将指导开发者,将问题分解为粗粒度的子问题,然后独立并行的借助blocks of threads来来解决。每个子问题分解为细粒度,可cooperatively parallel by all threads within the block来解决。这种分解有助于保持language expressivity,支持threads to cooperate when solving each sub-problem,同时支持自动扩展。
即:
CUDA中的函数不同于C++函数,称为kernels。
当kernels被调用时,可executed N times in parallel by N different CUDA threads,这一点不同于传统的C++函数。
kernel以__global__
来标记,CUDA threads的数量由<<<...>>>
execution configuration来指定。每个运行该kernel的thread都有a unique thread ID,可kernel可通过内置变量来访问。
如下例,借助内置变量threadIdx
,实现两个size为 N N N的向量 A + B = C A+B=C A+B=C。
// Kernel definition
__global__ void VecAdd(float* A, float* B, float* C)
{
int i = threadIdx.x;
C[i] = A[i] + B[i];
}
int main()
{
...
// Kernel invocation with N threads
VecAdd<<<1, N>>>(A, B, C);
...
}
threadIdx
为3-component vector,因此可使用one-dimensional thread index,或 two-dimensional thread index,或three-dimensional thread index来标记threads,所形成的的1-dimensional、2-dimensional或3-dimensional block of threads 称为a thread block。从而可invoke computation across the elements in a domain such as a vector, matrix, or volume。
以size为N*N的矩阵相加为例A+B=C:
// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
float C[N][N])
{
int i = threadIdx.x;
int j = threadIdx.y;
C[i][j] = A[i][j] + B[i][j];
}
int main()
{
...
// Kernel invocation with one block of N * N * 1 threads
int numBlocks = 1;
dim3 threadsPerBlock(N, N);
MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
...
}
每个block内的thread数量是有限的,因为一个block内的所有thread运行在同一processor core上,必须共享该core上有限的内存资源。当前GPU,threadsPerBlock
最多为1024。
但是,一个kernel可运行在多个block之上,总的threads数量等于numBlocks * threadsPerBlock
。因此numBlocks
取决于待处理的数据量。
blocks可以1维、2维或3维grid of thread blocks来表示:
当numBlocks>1
时,需借助内置变量blockIdx
和blockDim
:
// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
float C[N][N])
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
int j = blockIdx.y * blockDim.y + threadIdx.y;
if (i < N && j < N)
C[i][j] = A[i][j] + B[i][j];
}
int main()
{
...
// Kernel invocation
dim3 threadsPerBlock(16, 16); //每个thread block size为16x16(256 threads)
dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
...
}
thread block之间是相互独立的,可 以 任意顺序串行或并行执行。
block内的threads之间可通过共享内存来共享数据,可通过同步执行来coordinate memory accesses。
可通过调用__syncthreads()
内置函数来指定kernel中的synchronization points。__syncthreads()
可作为a barrier,该block内的所有threads必须wait before any is allowed to proceed。【__syncthreads()
的作用为:实现block内线程间的同步。除__syncthreads()
之外,Cooperative Groups API提供了更丰富的thread-synchronization primitives。】
为了高效协作,shared memory为离每个processor core很近的low-latency memory(更像是L1 cache),且__syncthreads()
应为轻量级的。
CUDA threads运行时可从多个memory space中访问数据:
CUDA编程模式中设定了:
Unified Memory可提供managed memory来bridge host memory space和device memory space。
Managed memory is accessible from all CPUs and GPUs in the system as a single, coherent memory image with a common address space.
默认,a kernel对应1个grid,可将该grid看成是三维立方体,gird内每个元素为一个block。若将grid描述为dim3 grid(10,10,2);
,则该grid内一共有10*10*2=200
个block。
每个block对应为一个三维的thread立方体。
通常,仅使用block立方体和thread立方体的x维度,对应为一维数组,此时,blockDim.x
对应为每个block的size,gridDim.x
为block的总数。
若launch kernel的参数为:
dim3 block_dim(128,1,1);
dim3 grid_dim(10,1,1);
kernel<<<grid_dim,block_dim>>>(...);
对应的:
若计算tid = threadIdx.x + blockIdx.x *blockDim.x
,则对应为 [0,128) + 128 * [0,9)
,对应tid
的取值范围为[0,1280)。当需要map threads to tasks时,这种方式可提供unique identifier for all of your threads in your kernel。
[1] 《CUDA C++ Programming Guide》
[2] NVCC
[3] 《Matrix computations on the GPU CUBLAS, CUSOLVER and MAGMA by example》
[4] 《Flexible matrix multiplication kernels on GPUs》
[5] Cuda gridDim and blockDim