2. CUDA 编程模型
1. CUDA 编程模型解决什么问题普通 C/C 程序通常由 CPU 执行。CUDA 编程模型允许程序员把适合并行处理的部分交给 GPU 执行。它主要解决两个问题如何让 CPU 和 GPU 协同工作如何描述一个需要大量并行执行的计算任务CUDA 编程模型并不要求程序员直接管理每个 GPU 硬件计算单元而是使用一套抽象概念描述任务Host / Device ↓ Kernel ↓ Grid / Block / Thread2. Host 与 DeviceCUDA 程序通常包含两个部分概念对应设备主要职责HostCPU控制程序流程、准备数据、启动 GPU 任务DeviceGPU执行适合并行处理的计算任务可以简单理解为Host 负责安排工作Device 负责完成计算整体关系如下CPUHost ├─ 准备输入数据 ├─ 将数据传给 GPU ├─ 启动 Kernel └─ 获取计算结果 GPUDevice └─ 执行 Kernel 中定义的并行计算需要注意Host 和 Device 不只是两个名称它们通常还对应不同的处理器和内存空间。因此CPU 中的数据通常不能直接当作 GPU 中的数据使用二者之间往往需要进行数据传输。3. Kernel 函数3.1 Kernel 是什么Kernel是一个由 CPU 发起、在 GPU 上执行的函数。普通函数通常由 CPU 调用resultadd(a,b);CUDA Kernel 则由 CPU 启动但函数主体在 GPU 上执行add1,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()kernelgrid, block()Kernel 的核心特点是一次启动可以让许多 GPU 线程执行同一段代码但每个线程处理不同的数据。4. 线程组织的基本形式CUDA 不直接把 GPU 任务描述为“启动若干个独立函数”而是把任务组织成多层结构Grid ├─ Block │ ├─ Thread │ ├─ Thread │ └─ ... ├─ Block │ └─ ... └─ ...这三个概念的含义是Thread执行最小任务的基本单位Block一组线程Grid一次 Kernel 启动所包含的全部 Block4.1 Thread一个线程通常负责处理一个数据元素或一小部分数据。例如向量加法中可以让线程 0 处理第 0 个元素 线程 1 处理第 1 个元素 线程 2 处理第 2 个元素4.2 BlockBlock 用于把线程分成若干组。例如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 的大小kernelgridSize,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 中的数据复制到 GPUcudaMemcpy(deviceData,hostData,N*sizeof(int),cudaMemcpyHostToDevice);其中cudaMemcpyHostToDevice表示从 Host 复制到 DevicecudaMemcpyDeviceToHost表示从 Device 复制到 Host第四步启动 KernelkernelgridSize,blockSize(deviceData);这一步告诉 GPU请按照指定的线程组织方式执行这个 Kernel。第五步获取结果如果 GPU 计算出了结果可以将结果复制回 CPUcudaMemcpy(hostData,deviceData,N*sizeof(int),cudaMemcpyDeviceToHost);第六步释放资源使用完 GPU 内存后需要释放资源cudaFree(deviceData);7. 编程模型与执行模型的区别这两个概念容易混淆。CUDA 编程模型关注什么CUDA 编程模型关注CPU 和 GPU 如何分工如何编写和启动 Kernel如何组织 Thread、Block 和 Grid如何管理 Host 与 Device 之间的数据可以概括为程序员如何描述一个并行计算任务。CUDA 执行模型关注什么CUDA 执行模型关注GPU 如何执行线程Thread 如何组成 WarpWarp 如何在 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 组织并行计算。需要掌握的核心关系是HostCPU 端程序 DeviceGPU 端程序和数据 Kernel在 GPU 上执行的函数 Thread执行最小计算任务的单位 BlockThread 的集合 GridBlock 的集合典型 CUDA 程序流程是准备数据 → 分配 Device 内存 → 传输数据 → 启动 Kernel → 获取结果 → 释放资源