CUDA编程学习笔记-already_true

主要参考CUDA编程入门极简教程 , CUDA从入门到精通,CUDA——从入门到放弃,CUDA编程入门

推荐书籍:《GPU高性能编程CUDA实战》(可操作性强)、《GPGPU编程技术》(全面客观详细介绍通用GPU编程的策略)、《OpenGL编程指南》(图形交互)、《GPU高性能运算之CUDA》(快速查询关键技术和概念)、各种工具使用手册

Prerequisite

根据费林分类法(Flynn’s Taxonomy),可以将资讯流(information stream)分成指令(Instruction)和数据(Data)两种,据此又可分成四种计算机类型:

  • 单一指令流单一数据流计算机(SISD):单核CPU
  • 单一指令流多数据流计算机(SIMD):GPU的计算模型
  • 多指令流单一数据流计算机(MISD):流水线模型
  • 多指令流多数据流计算机(MIMD):多核CPU

中央处理器(CPU, Central Processing Unit)

计算机的运算核心(Core)和控制核心(Control Unit),主要包括运算器(ALU, Arithmetic Logic Unit),控制单元(CU, Control Unit),寄存器(Register),高速缓冲存储器(Cache)以及实现二者联系的数据(Data)、控制及状态的总线(Bus)。
CPU与内部存储器(Memory)以及输入输出(I/O)设备合称为电子计算机的三大核心部件。

CPU遵循的是冯诺依曼架构,其核心就是:存储程序,顺序执行。

显卡(Video card, Graphic card)

显示接口卡、显示适配器,是计算机进行数模信号转换的设备,承担输出显示图形的任务。同时具有图像处理能力,协助CPU工作,提高整体运行速度。

GPU(Graphic Processing Unit)是显卡上的一块芯片,最初仅用于图像渲染。后提出GPGPU(General Purpose GPU)的概念。GPU无法单独工作,必须由CPU进行控制调用才能工作。


CUDA (Compute Unified Devices Architecture)是NVIDIA公司开发的GPU编程模型,提供了GPU编程的简易接口。它将GPU视作一个数据并行计算设备,基于CUDA编程可以构建基于GPU计算的应用程序。CUDA提供了对多种编程语言的支持,如C/C++,Python等。

CUDA编程模型基础

在异构计算架构中,GPU与CPU通过PCIe总线连接在一起来协同工作,CPU所在位置称为为主机端(host),而GPU所在位置称为设备端(device)。GPU包含更多的运算核心,特别适合进行数据并行的计算密集型任务,CPU可以实现复杂的逻辑运算,适合控制密集型任务。

在CUDA程序架构中,主程序由CPU执行,数据并行处理部分被编译成GPU能执行的程序传送到GPU中。以上被编译的程序在CUDA中被称为kernel,CUDA允许程序员定义C语言kernel函数。

在调用kernel函数时,它由 N N N个不同的CUDA线程并行执行 N N N次;执行kernel的每个线程都会被分配一个独特的线程ID——通过内置threadIdx变量访问。

显卡利用率查看方式

> nvidia-smi

线程层次结构

SP (Streaming Processor):也称为CUDA core,是最基本的处理单元。具体的指令和任务都是在SP上处理的。GPU进行并行计算,也就是很多个SP同时做处理。
SM (Streaming Multiprocessor):多个SP加上其他的一些资源,如warp scheduler, register, shared memory等。从软件上看,SM像一个独立的CPU core。

线程(Thread):一般通过GPU的一个核进行处理。
线程块(Block):软件概念,一个block只会由一个SM调度。多个threads组成,各个block并行执行但无法通信。
线程格(Grid):多个blocks组成。
线程束(Warp):一个包含32个thread的集合,该集合被编织在一起且以步调一致的形式执行。warp是调度和运行的基本单元。在程序中的每一行,线程束中的每个线程都将在不同数据上执行相同的命令——SIMT (Single Instruction Multiple Threads)。

在GPU中要执行的线程,根据最有效的数据共享来创建块(Block)。
在同一个block内的线程可以彼此协作,通过一些共享存储器共享数据,并通过同步执行来协调存储器访问。
一个block中的所有线程必须位于同一个处理器核心中,即一个处理器核心的有限存储器资源限制了每个block的线程数量。

一个内核可由多个大小相同的线程块同时执行。
线 程 总 数 = 线 程 p e r _ b l o c k × # b l o c k s 线程总数=线程per\_block\times \#blocks 线=线per_block×#blocks

一个SM可以同时拥有多个blocks,但是需要序列执行。

大部分threads只是逻辑上并行,并不是所有的thread可以在物理上同时执行。同一个warp中的thread可以以任意顺序执行,active warps被sm资源限制。当一个warp空闲时,SM就可以调度驻留在该SM中另一个可用warp。

存储器层次结构

CUDA设备拥有多个独立的存储空间。

主机(Host):CPU及系统内存。
设备(Device):GPU及GPU本身的显示内存。
DRAM (Dynamic Random Access Memory):最常见的系统内存。

CUDA线程可在执行过程中访问多个存储器空间的数据:

  • 每个thread都有一个private 本地存储器
  • 每个block都有一个共享存储器,对块内的所有线程可见,且与block具有相同的生命周期
  • 所有thread可访问相同的全局存储器
  • 两个read-only存储器,可由所有线程访问。

并行计算

并行性

block可以按任意顺序执行。grid中的block可以被分配到任意一个由空闲部分的SM上。

局部性

缓存一致性

对于“缓存一致”的系统,一个内存的写操作需要通知所有核的各个级别的缓存。因此,无论何时,所有的处理器核看到的内存视图是完全一样的。非“缓存一致”系统不会自动地更新其他核的缓存。它需要由程序员写清楚每个处理器核输出的各自不同的目标区域。
通常,CPU遵循“缓存一致性”原则,而GPU则不是。故GPU能够扩展到一个芯片内具有大数量的核心。

CUDA编程

NVIDIA官方教程, Programming Guide :: CUDA Toolkit Documentation

CUDA编程模型是一个异构模型,需要CPU和GPU协同工作。典型的CUDA程序执行流程:

  1. 分配host内存,进行数据初始化
  2. 分配device内存,从host将数据拷贝到device上
  3. 调用CUDA的kernel函数在device上完成指定运算
  4. 将device上的运算结果拷贝到host上
  5. 释放device和host上分配的内存

CUDA C

对C/C++语言进行拓展后形成的变种,兼容C/C++语法,文件类型为.cu文件,编译器为nvcc,相比传统的C/C++,主要添加了以下几个方面:

  • 函数类型限定符
  • 执行配置运算符
  • 内置变量(五个)
  • 变量类型限定符
  • 各种函数等

变量类型限定符

确定某个变量在设备上的内存位置.

  • __device__表示位于全局内存空间,默认类型
  • __share__表示位于共享内存空间
  • __constant__表示位于常量内存空间
  • __texture__表示其绑定的变量可以被纹理缓存加速访问
  • __managed__表示

函数类型限定符

在使用 CUDA 之后,我们获得了 GPU 的控制权,现在在编写代码时需要指明是 CPU 还是 GPU 进行数据运算。我们可以简单的将数据运算(即函数的调用方式)分为三种:

  1. global 在CPU上调用函数,函数在GPU上执行(异步)
  2. device 在GPU上调用函数,函数在GPU上执行
  3. host 在CPU上调用函数,函数在CPU上执行(同步)
__global__ void global_func(float func_input){
	// Something
}
__host__ void MyFunc(int func_input){
	// Something
}
__device__ void MyFunc(byte func_input){
	//Something
}

kernel是在device上的线程中并行执行的函数,采用__global__符号声明,调用时采用<<>>指定kernel需要执行的线程数量。

Get the computation running (in parallel) on the many cores of a GPU.
First, turn target function into a function that the GPU can run, called a kernel in CUDA – add the specifier __global__ to the function which tells the CUDA C++ compiler that this is a function that runs on the GPU and can be called from CPU code.

// CUDA Kernel function
__global__
void add(int n, float *x, float *y){
	for(int i=0; i<n; i++)
		y[i] = x[i] + y[i];
}

内存分配

Unified Memory in CUDA provides a single memory space accessible by all GPUs and CPUs in your system.
To allocate data in unified memory, call cudaMallocManaged(), which returns a pointer that you can access from host (CPU) code or device (GPU) code. To free the data, just pass the pointer to cudaFree().

// Allocate Unified Memory
float *x, *y;
cudaMallocManaged(&x, N*sizeof(float));
cudaMallocManaged(&y, N*sizeof(float));

// Free memory
cudaFree(x);
cudaFree(y);

执行配置运算符

执行配置运算符<<<>>>用来传递kernel函数的执行参数,格式如下:
kernel<<>>(param1, param2,...);
memSize表示动态分配的共享存储器大小,默认为0;stream表示执行流,默认为0。

内置变量

用于用来在运行时获得Grid和Block的尺寸及线程索引等信息。

  • gridDim:包含三个元素x, y, z的结构体,表示Grid在三个方向上的尺寸
  • blockDim:包含上元素x, y, z的结构体,表示Block在三个方向上的尺寸
  • blockIdx:包含三个元素x, y, z的结构体,分别表示当前线程所在块在网格中x, y, z方向上的索引
  • threadIdx:包含三个元素x, y, z的结构体,分别表示当前线程在其所在块中x, y, z方向上的索引
  • warpSize:表示warp的尺寸。

Launch the add() kernel. CUDA kernel launches are specified using the triple angle bracket syntax <<< >>>

add<<<1, 1>>>(N, x, y); // this launches one GPU thread to run this instruction

You need the CPU to wait until the kernel is done before it accesses the results (because CUDA kernel launches don’t block the calling CPU thread). To do this, call cudaDeviceSynchronize() before doing the final error checking on the CPU.

The complete code:

// "add.cu" compile it with nvcc.
#include 
#include 
//Kernel function 
__global__
void add(int n, float *x, float *y){
	for(int i=0; i<n; i++)
		y[i] = x[i] + y[i];
}

int main(void){
	int N = 1 << 20;
	float *x, *y;

	// Allocate Unified Memory
	cudaMallocManaged(&x, N*sizeof(float));
	cudaMallocManaged(&y, N*sizeof(float));
	
	// Initialize x and y arrays on the host
	for(int i=0; i<N; i++){
		x[i] = 1.0f;
		y[i] = 2.0f;
	}
	// Run kernel on 1M elements on the GPU
	add<<<1, 1>>>(N, x, y);
	// Wait for GPU to finish
	cudaDeviceSynchronize();
	// Check for errors
	float maxError = 0.0f;
	for(int i=0; i<N; i++)
		maxError = fmax(maxError, fabs(y[i] - 3.0f));
	std::cout << "Max error: " << maxError << std::endl;
	// Free memory
	cudaFree(x);
	cudaFree(y);
	return 0;
}



Picking up the Threads

The execution configuration: <<<#Blocks, #threadsInABlock>>>, it tells the CUDA runtime how many parallel threads to use for the launch on the GPU.

CUDA C++ provides keywords that let kernels get the indices of the running threads. Specifically, threadIdx.x contains the index of the current thread within its block, and blockDim.x contains the number of threads in the block.

// add_block.cu
__global void add(int n, float *x, float *y){
	int index = threadIdx.x;
	int stride = blockDim.x;
	for (int i=index; i<n; i+=stride)
		y[i] = x[i] + y[i];
}

Out of the Blocks

CUDA GPUs have many parallel processors grouped into Streaming Multiprocessors, or SMs. Each SM can run multiple concurrent thread blocks. To take full advantage of all these threads, you should launch the kernel with multiple thread blocks.

CUDA provides gridDim.x, which contains the number of blocks in the grid, and blockIdx.x, which contains the index of the current thread block in the grid.

// add_grid.cu
int blockSize = 256; // the number of threads in a single block
int numBlocks = (N + blockSize - 1) / blockSize; // round-up number of blocks needed
add<<<numBlocks, blockSize>>>(N, x, y);

__global__ void add(int n, float *x, float *y){
	int index = blockIdx.x * blockDim.x + threadIdx.x;
	int stride = blockDim.x * gridDim.x; // the total number of threads in the grid
	// a grid-stride loop
	for(int i=0; i<n; i+=stride)
		y[i] = x[i] + y[i];
}

CUDA编程学习笔记-already_true_第1张图片

CUDA库

CUDA math:常用数学运算
cuBLAS:矩阵运算

你可能感兴趣的:(cuda)