ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2. CUDA 编程模型

2. CUDA 编程模型

1. CUDA 编程模型解决什么问题?

普通 C/C++ 程序通常由 CPU 执行。CUDA 编程模型允许程序员把适合并行处理的部分交给 GPU 执行。

它主要解决两个问题:

  1. 如何让 CPU 和 GPU 协同工作?
  2. 如何描述一个需要大量并行执行的计算任务?

CUDA 编程模型并不要求程序员直接管理每个 GPU 硬件计算单元,而是使用一套抽象概念描述任务:

Host / Device ↓ Kernel ↓ Grid / Block / Thread

2. Host 与 Device

CUDA 程序通常包含两个部分:

概念对应设备主要职责
HostCPU控制程序流程、准备数据、启动 GPU 任务
DeviceGPU执行适合并行处理的计算任务

可以简单理解为:

  • 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 发起调用
执行位置CPUGPU
执行方式通常按一次函数调用执行通常由大量线程并行执行
调用形式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 中有多少个 Block
  • blockSize表示每个 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 编程模型的核心思想:

  1. 把一个大任务拆分为许多小任务
  2. 让每个线程负责一部分数据
  3. 让多个线程执行相同的 Kernel 代码
  4. 通过线程索引确定每个线程处理哪部分数据

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 复制到 Device
  • cudaMemcpyDeviceToHost表示从 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 → 获取结果 → 释放资源
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进