1. CUDA 编程模型解决什么问题?
普通 C/C++ 程序通常由 CPU 执行。CUDA 编程模型允许程序员把适合并行处理的部分交给 GPU 执行。
它主要解决两个问题:
- 如何让 CPU 和 GPU 协同工作?
- 如何描述一个需要大量并行执行的计算任务?
CUDA 编程模型并不要求程序员直接管理每个 GPU 硬件计算单元,而是使用一套抽象概念描述任务:
Host / Device ↓ Kernel ↓ Grid / Block / Thread2. Host 与 Device
CUDA 程序通常包含两个部分:
| 概念 | 对应设备 | 主要职责 |
|---|---|---|
| Host | CPU | 控制程序流程、准备数据、启动 GPU 任务 |
| Device | GPU | 执行适合并行处理的计算任务 |
可以简单理解为:
- Host 负责安排工作
- Device 负责完成计算
整体关系如下:
CPU(Host) ├─ 准备输入数据 ├─ 将数据传给 GPU ├─ 启动 Kernel └─ 获取计算结果 GPU(Device) └─ 执行 Kernel 中定义的并行计算需要注意:
Host 和 Device 不只是两个名称,它们通常还对应不同的处理器和内存空间。
因此,CPU 中的数据通常不能直接当作 GPU 中的数据使用,二者之间往往需要进行数据传输。
3. Kernel 函数
3.1 Kernel 是什么?
Kernel是一个由 CPU 发起、在 GPU 上执行的函数。
普通函数通常由 CPU 调用:
result=add(a,b);CUDA Kernel 则由 CPU 启动,但函数主体在 GPU 上执行:
add<<<1,256>>>(a,b,result);其中:
add是 Kernel 函数<<<1, 256>>>描述本次启动的线程组织方式a、b和result是传递给 Kernel 的参数
Kernel 通常使用__global__修饰:
__global__voidadd(int*a,int*b,int*result){// 在 GPU 上执行的代码}3.2 Kernel 与普通函数的区别
| 对比项 | 普通函数 | Kernel |
|---|---|---|
| 调用位置 | 通常由 CPU 调用 | 由 CPU 发起调用 |
| 执行位置 | CPU | GPU |
| 执行方式 | 通常按一次函数调用执行 | 通常由大量线程并行执行 |
| 调用形式 | function() | kernel<<<grid, block>>() |
Kernel 的核心特点是:
一次启动可以让许多 GPU 线程执行同一段代码,但每个线程处理不同的数据。
4. 线程组织的基本形式
CUDA 不直接把 GPU 任务描述为“启动若干个独立函数”,而是把任务组织成多层结构:
Grid ├─ Block │ ├─ Thread │ ├─ Thread │ └─ ... ├─ Block │ └─ ... └─ ...这三个概念的含义是:
- Thread:执行最小任务的基本单位
- Block:一组线程
- Grid:一次 Kernel 启动所包含的全部 Block
4.1 Thread
一个线程通常负责处理一个数据元素或一小部分数据。
例如,向量加法中可以让:
线程 0 处理第 0 个元素 线程 1 处理第 1 个元素 线程 2 处理第 2 个元素4.2 Block
Block 用于把线程分成若干组。
例如:
Block 0:线程 0 ~ 255 Block 1:线程 256 ~ 511 Block 2:线程 512 ~ 767使用 Block 可以让程序更清晰地组织大量线程。
4.3 Grid
一次 Kernel 启动时,所有参与计算的 Block 组成一个 Grid。
例如:
Grid ├─ Block 0 ├─ Block 1 ├─ Block 2 └─ Block 3启动 Kernel 时,通常需要指定 Grid 和 Block 的大小:
kernel<<<gridSize,blockSize>>>(args);这里:
gridSize表示 Grid 中有多少个 BlockblockSize表示每个 Block 中有多少个 Thread
本次只需要理解它们的组织关系,不讨论 GPU 如何实际调度这些线程。
5. 一个简单例子:向量加法
假设有两个数组:
A = [1, 2, 3, 4] B = [10, 20, 30, 40]希望计算:
C[i] = A[i] + B[i]可以按照以下方式分配任务:
Thread 0:计算 C[0] = A[0] + B[0] Thread 1:计算 C[1] = A[1] + B[1] Thread 2:计算 C[2] = A[2] + B[2] Thread 3:计算 C[3] = A[3] + B[3]对应的 Kernel 可以简化为:
__global__voidvectorAdd(constint*a,constint*b,int*c){inti=/* 当前线程对应的索引 */;c[i]=a[i]+b[i];}这个例子体现了 CUDA 编程模型的核心思想:
- 把一个大任务拆分为许多小任务
- 让每个线程负责一部分数据
- 让多个线程执行相同的 Kernel 代码
- 通过线程索引确定每个线程处理哪部分数据
6. CUDA 程序的基本工作流程
一个典型 CUDA 程序通常按照以下流程运行:
准备 CPU 数据 ↓ 分配 GPU 内存 ↓ 将数据复制到 GPU ↓ 启动 Kernel ↓ 将结果复制回 CPU ↓ 释放 GPU 资源第一步:准备 Host 数据
CPU 首先创建输入数据:
inthostData[N];这些数据位于 Host 端,也就是 CPU 可以直接访问的内存中。
第二步:准备 Device 内存
程序需要为 GPU 分配对应的内存:
int*deviceData;cudaMalloc(&deviceData,N*sizeof(int));cudaMalloc用于在 Device 端分配内存。
第三步:传输数据
将 CPU 中的数据复制到 GPU:
cudaMemcpy(deviceData,hostData,N*sizeof(int),cudaMemcpyHostToDevice);其中:
cudaMemcpyHostToDevice表示从 Host 复制到 DevicecudaMemcpyDeviceToHost表示从 Device 复制到 Host
第四步:启动 Kernel
kernel<<<gridSize,blockSize>>>(deviceData);这一步告诉 GPU:
请按照指定的线程组织方式,执行这个 Kernel。
第五步:获取结果
如果 GPU 计算出了结果,可以将结果复制回 CPU:
cudaMemcpy(hostData,deviceData,N*sizeof(int),cudaMemcpyDeviceToHost);第六步:释放资源
使用完 GPU 内存后,需要释放资源:
cudaFree(deviceData);7. 编程模型与执行模型的区别
这两个概念容易混淆。
CUDA 编程模型关注什么?
CUDA 编程模型关注:
- CPU 和 GPU 如何分工
- 如何编写和启动 Kernel
- 如何组织 Thread、Block 和 Grid
- 如何管理 Host 与 Device 之间的数据
可以概括为:
程序员如何描述一个并行计算任务。
CUDA 执行模型关注什么?
CUDA 执行模型关注:
- GPU 如何执行线程
- Thread 如何组成 Warp
- Warp 如何在 SM 上运行
- GPU 如何调度线程
- SIMT 执行方式是什么
可以概括为:
GPU 硬件如何实际执行程序员描述的并行任务。
二者的关系是:
编程模型:程序员如何组织任务 ↓ 执行模型:GPU 如何执行任务本次学习的重点是编程模型,暂不深入 Warp、SM 和 SIMT。
8. 常见误解
误解一:一个 Kernel 只执行一次
不一定。
一次 Kernel 启动通常会创建许多线程,多个线程会执行同一个 Kernel 函数。
误解二:Thread、Block 和 Grid 是硬件核心
不是。
它们是 CUDA 提供的任务组织抽象,用来帮助程序员描述并行任务。
误解三:CPU 可以直接使用 GPU 内存中的数据
通常不能直接使用。
CPU 和 GPU 通常拥有不同的内存空间,程序需要根据需要进行数据传输。
误解四:编程模型就是执行模型
不是。
- 编程模型描述程序如何组织任务
- 执行模型描述 GPU 如何实际执行任务
误解五:使用 Kernel 就一定能获得加速
不一定。
最终性能还会受到以下因素影响:
- 数据传输成本
- 任务规模
- 线程组织方式
- GPU 的实际执行效率
这些内容属于后续的执行模型和性能优化部分。
9. 总结
CUDA 编程模型可以概括为:
CPU 负责控制程序和启动任务,GPU 负责执行 Kernel;程序员通过 Grid、Block 和 Thread 组织并行计算。
需要掌握的核心关系是:
Host:CPU 端程序 Device:GPU 端程序和数据 Kernel:在 GPU 上执行的函数 Thread:执行最小计算任务的单位 Block:Thread 的集合 Grid:Block 的集合典型 CUDA 程序流程是:
准备数据 → 分配 Device 内存 → 传输数据 → 启动 Kernel → 获取结果 → 释放资源