文章目录
本文介绍GPU编程的基础,包括一些基本术语以及如何编写一个kernel实现两个矩阵的加法
基础术语
Kernel
kernel是GPU跑的的一个特殊的函数,GPU有很多同时工作的线程,而kernel就像发给他们的指令。我们通常用__global__ 关键字来标识一个kernel,其返回值只能为空
cpp
__global__ void addNumbers(int *a, int *b, int *result) {
*result = *a + *b;
}
Grid
grid代表单个kernel 召唤时的所有线程集/线程块,是整个执行空间。当你启动kernel,你需要指定grid的维度,需要定义有多少blocks会被创建,可以是1D,2D或者3D
比如:处理一个大图像,每个block可以处理图像的一个16x16的区域
Block
block是一组线程,他们可以合作和快速在共享内存中分享数据1D,2D或者3D。
一个block的线程可以:
- 共享内存
- 互相之间同步
- 在任务上协作
Threads
线程是最小的执行单元,每个线程独立执行kernel 代码. 在一个block里,线程被一个独特的线程ID唯一定义,这个ID让你可以获取某个数据或者根据线程在block的位置执行不同的操作
理解CUDA的线程索引
刚刚提到"在一个block里,线程被一个独特的线程ID唯一定义",如下的变量通常被用来定义这个ID:
-
threadIdx:- 一个三元组 (
threadIdx.x,threadIdx.y,threadIdx.z) 描述了线程在一个block的位置.
- 一个三元组 (
-
blockDim:- 一个三元组 (
blockDim.x,blockDim.y,blockDim.z) 指定了block的维度
- 一个三元组 (
-
blockIdx:- 一个三元组(
blockIdx.x,blockIdx.y,blockIdx.z) 描述了block在Grid中的位置.
- 一个三元组(
-
gridDim:- 一个三元组 (
gridDim.x,gridDim.y,gridDim.z) 指定了Grid的维度
- 一个三元组 (
计算全局的线程ID
对于一个一维的线程ID,可以通过如下公式计算:
cpp
int globalThreadId = blockIdx.x * blockDim.x + threadIdx.x;
*辅助类型和函数
dim3 描述之前三元组的简单方式
cpp
dim3 blockSize(16, 16, 1); // 16x16x1 threads per block
dim3 gridSize(8, 8, 1); // 8x8x1 blocks in grid
<<<>>>
一个启动kernel的特殊括号,指定了grid size和block size, shared memory size, and stream for kernel execution. 合理配置这些参数对于GPU编程和性能优化是至关重要的
cpp
addNumbers<<<gridSize, blockSize>>>(a, b, result);
GPU内存以及管理
GPU内存的类型
global memory:- 主存,所有线程共享
- 慢但尺寸大
- 用来所有线程共享数据
例如:大矩阵或者数据集
shared memory:- 每个block中所有线程共享的内存
- 非常快但尺寸小
- 用来做block内所有线程需要频繁共享的数据
例如: 临时变量或者小的LUT(查找表)
registers:- 最快的内存,每个线程独占
- 存放临时变量
- 数量很少,所以要用好它们
例如:循环变量或者计算的中间结果
constant memory:- 所有线程的只读内存
- 用来存放在kernel运行期间不变的常量
- 目的是缓存来做快速存取
例如:常量或者配置参数
local memory:- 当寄存器不够(spilling)时使用
- 慢(存放在全局内存)
- 如果可以请避免
例如:装不下寄存器的大矩阵或者变量
内存管理
cudaMalloc: 为GPU分配内存
cpp
int *device_array;
cudaMalloc(&device_array, size * sizeof(int));
cudaMemcpy
It can copy from device to host, host to device, or device to device (edge cases)
-
host to device ⇒ CPU to GPU
-
device to host ⇒ GPU to CPU
-
device to device ⇒ GPU location to different GPU location
-
cudaMemcpyHostToDevice,cudaMemcpyDeviceToHost, orcudaMemcpyDeviceToDevice -
cudaFreewill free memory on the device -
Example:
cpp
cudaMemcpy(device_array, host_array, size * sizeof(int), cudaMemcpyHostToDevice);
cudaDeviceSynchronize()
默认 CPU and GPU 一起工作降低延迟,有时候我们需要CPU等待GPU,我们需要使用 cudaDeviceSynchronize().
- 适合需要结果才能继续的场景
- 例如在运行某些benchmark的时候:
cpp
kernel<<<gridSize, blockSize>>>(data);
cudaDeviceSynchronize(); // Wait for kernel to finish
printf("Kernel completed!");
综合应用
如下是两个数组相加的kernel实现,先定义了每个block 256个线程,然后计算grid的大小(向上取整)并且线程内设置了防止数组越界的限制
cpp
// Kernel definition
__global__ void addArrays(int *a, int *b, int *c, int size) {
// Calculate unique index for this thread
int index = blockIdx.x * blockDim.x + threadIdx.x;
// Make sure we don't go past array bounds
if (index < size) {
c[index] = a[index] + b[index];
}
}
// In main function:
dim3 blockSize(256); // 256 threads per block
dim3 gridSize((size + blockSize.x - 1) / blockSize.x); // Calculate needed blocks
addArrays<<<gridSize, blockSize>>>(d_a, d_b, d_c, size);