一、什么是GPU Kernel
Kernel(内核)是在GPU上并行执行的函数。与CPU上只执行一次的传统C/C++函数不同,GPU kernel会被成千上万个线程同时执行,每个线程执行相同的代码但处理不同的数据。
在CUDA中,kernel通过__global_
_关键字标识,调用时使用特殊的<<<...>>>语法指定线程数量。
二、Kernel的线程层次结构
2.1 三层架构:Thread → Block → Grid
GPU将线程组织成三级层次结构:

Thread(线程):最小的执行单元,每个线程执行kernel代码的一个副本,拥有独立的寄存器和程序计数器。
Block(线程块):一组线程的集合,同一block内的线程可以:
-
通过共享内存高效交换数据
-
使用
__syncthreads()进行同步 -
必须在同一个流式多处理器(SM)上执行
每个block最多包含1024个线程。
Grid(网格):所有block的集合,一个kernel启动的所有线程构成一个grid。Grid的维度可以是一维、二维或三维。
2.2 内置变量与索引计算
每个线程通过内置变量获取自己的身份信息:
| 变量 | 含义 |
|---|---|
threadIdx |
线程在block内的索引 |
blockIdx |
block在grid内的索引 |
blockDim |
block的维度(线程数) |
gridDim |
grid的维度(block数) |
全局线程ID计算公式(一维情况):
global_thread_id = blockIdx.x * blockDim.x + threadIdx.x
三、Kernel的启动与执行流程
3.1 总体流程
一个完整的CUDA程序执行流程如下:
-
分配主机内存和GPU显存
-
初始化主机数据
-
将数据从主机内存复制到GPU显存
-
启动kernel执行计算
-
将结果从GPU显存复制回主机内存
-
释放内存
3.2 Kernel启动的底层机制("邮局模型")
Kernel的启动可以类比为CPU向GPU"投递邮包":
第一步:准备命令包:CPU将kernel的函数指针、网格/线程块维度、参数等打包成特定格式的命令包。
第二步:投递到命令队列:命令包被放入驻留在内存中的命令队列(环形缓冲区)。
第三步:敲响门铃(Doorbell):CPU通过写特定的内存映射I/O寄存器通知GPU"有新任务"。
第四步:硬件调度器接管:GPU前端的硬件调度器(NVIDIA的GigaThread Engine)通过DMA从队列中拉取命令包,kernel正式进入设备端执行。
四、硬件执行模型:SM与Warp
4.1 流式多处理器(SM)
GPU由多个流式多处理器(SM)组成,每个SM包含:
-
CUDA核心阵列:执行实际算术运算
-
寄存器文件:每个线程的私有存储
-
共享内存:block内线程共享
-
调度单元:负责分配任务
当kernel启动时,block被分发到不同的SM上执行。一个block不能被拆分到多个SM,但多个block可以分配到同一个SM。
4.2 Warp:真正的执行单元
Warp是GPU调度的基本单位 。每个warp包含32个连续线程,这些线程:
-
同步执行(锁步):同一时刻执行相同指令
-
共享程序计数器:所有线程从同一地址取指令
-
拥有独立寄存器:每个线程有自己的数据
当block被分配给SM后,SM将其中的线程划分为warp。如果block大小不是32的倍数,最后一个warp会有空闲线程。
Warp调度:SM中的warp调度器每个时钟周期选择一个就绪的warp执行。warp有三种状态:
-
Selected:正在执行
-
Eligible:准备就绪,等待执行
-
Stalled:因等待数据等原因停滞
4.3 SIMT架构
GPU采用**SIMT(单指令多线程)**架构:一条指令控制多个线程执行。
Warp分歧(Divergence) :当warp内线程遇到条件分支走向不同路径时,GPU会串行执行各分支路径,禁用不在当前路径上的线程。这会导致性能下降,因此应尽量避免warp内的分支分歧。
4.4 延迟隐藏(Latency Hiding)
GPU通过大量active warp实现延迟隐藏:当某些warp因内存访问停滞时,调度器立即切换到其他就绪warp执行。这就像流水线------某个工位等待材料时,其他工位继续工作,整体效率不受影响。
每个SM可驻留的warp数量有限(通常64个),更多的active warp意味着更好的延迟隐藏能力。
五、GPU内存层次结构
GPU的内存层次结构遵循"离ALU越近,越快、越小、越贵"的原则:
5.1 各级内存概览
| 内存类型 | 作用域 | 访问速度 | 典型大小 |
|---|---|---|---|
| 寄存器 | 每线程私有 | 最快(1周期) | 每SM数百KB |
| 共享内存 | 每block共享 | 极快(几周期) | 每SM 64-164KB |
| L1缓存 | 每SM | 快 | 几十KB |
| L2缓存 | 全局 | 中等 | 几MB到几十MB |
| 全局内存 | 所有线程可访问 | 慢(数百周期) | 几GB到几十GB |
| 常量内存 | 所有线程只读 | 缓存命中时快 | 64KB |
5.2 各级内存详解
寄存器:每个线程私有的最快存储,用于存放频繁使用的局部变量。
共享内存(Shared Memory):同一block内所有线程共享,是线程间通信延迟最小的方法。由程序员显式管理。
全局内存(Global Memory):所有线程均可读写,是所有数据的"仓库",容量最大但延迟最高。
常量内存(Constant Memory):只读,适合所有线程访问相同数据的场景。
六、完整示例:向量加法
6.1 Kernel代码
cpp
__global__ void vectorAdd(const float* A, const float* B, float* C, int N) {
// 计算全局线程ID
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 边界检查,防止越界
if (idx < N) {
C[idx] = A[idx] + B[idx];
}
}
6.2 主机端调用代码
cpp
int main() {
int N = 1000000;
size_t bytes = N * sizeof(float);
// 1. 分配主机内存
float *h_A = (float*)malloc(bytes);
float *h_B = (float*)malloc(bytes);
float *h_C = (float*)malloc(bytes);
// 2. 初始化数据
for (int i = 0; i < N; i++) {
h_A[i] = rand() / (float)RAND_MAX;
h_B[i] = rand() / (float)RAND_MAX;
}
// 3. 分配GPU显存
float *d_A, *d_B, *d_C;
cudaMalloc(&d_A, bytes);
cudaMalloc(&d_B, bytes);
cudaMalloc(&d_C, bytes);
// 4. 将数据从主机复制到GPU
cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);
// 5. 配置并启动kernel
int threadsPerBlock = 256;
int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
// 6. 等待GPU完成
cudaDeviceSynchronize();
// 7. 将结果从GPU复制回主机
cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);
// 8. 释放内存
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
free(h_A); free(h_B); free(h_C);
return 0;
}
6.3 执行过程分解
Step 1 - 数据准备:主机分配内存并初始化A、B向量。
Step 2 - 数据传输:通过PCIe总线将数据从主机内存复制到GPU全局内存。
Step 3 - Kernel启动 :<<<blocksPerGrid, threadsPerBlock>>>配置启动网格。
Step 4 - Block分发:GigaThread Engine将block分发到各SM。假设有100万个元素、每block256线程,约3907个block被分发。
Step 5 - Warp划分:每个SM收到block后,将256线程划分为8个warp(256/32=8)。
Step 6 - 指令执行 :warp调度器每个周期选择一个warp执行。32个线程同时执行C[idx] = A[idx] + B[idx]。
Step 7 - 结果返回:GPU计算完成,数据从全局内存复制回主机。
七、优化关键点
7.1 内存合并访问(Coalesced Access)
相邻线程应访问连续的内存地址,使硬件能将多个访问合并为一次事务。
7.2 共享内存使用
将频繁访问的全局内存数据加载到共享内存,减少对慢速全局内存的访问。
7.3 占用率(Occupancy)
每个SM上活跃warp与最大可能warp数的比率。更高的占用率有助于更好地隐藏延迟。
7.4 避免Warp分歧
尽量让warp内32个线程走相同的执行路径。
八、总结
GPU kernel的执行是一个从软件抽象到硬件实现的完整流程:
-
软件层面 :程序员编写
__global__函数,通过<<<>>>指定线程组织结构 -
运行时层面:CPU将kernel打包为命令包,通过队列投递给GPU
-
硬件层面:GPU的SM将block划分为warp,warp调度器以SIMT方式执行
-
内存层面:数据在寄存器、共享内存、全局内存间流动
理解这一完整链条,是写出高性能GPU程序的关键。从线程网格的精心设计,到warp分歧的谨慎规避,再到内存访问模式的深度优化,每一步都直接影响着最终的计算效率。