CUDA编程之Kernal

一、什么是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程序执行流程如下:

  1. 分配主机内存和GPU显存

  2. 初始化主机数据

  3. 将数据从主机内存复制到GPU显存

  4. 启动kernel执行计算

  5. 将结果从GPU显存复制回主机内存

  6. 释放内存

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的执行是一个从软件抽象到硬件实现的完整流程:

  1. 软件层面 :程序员编写__global__函数,通过<<<>>>指定线程组织结构

  2. 运行时层面:CPU将kernel打包为命令包,通过队列投递给GPU

  3. 硬件层面:GPU的SM将block划分为warp,warp调度器以SIMT方式执行

  4. 内存层面:数据在寄存器、共享内存、全局内存间流动

理解这一完整链条,是写出高性能GPU程序的关键。从线程网格的精心设计,到warp分歧的谨慎规避,再到内存访问模式的深度优化,每一步都直接影响着最终的计算效率。

相关推荐
xin_yao_xin1 天前
Conda 环境的 CUDA PATH 配置指南
开发语言·python·conda·cuda
basketball61622 天前
AI Infra 硬件体系与编程模型:17. CUDA编程基础:底层驱动 API 调用
人工智能·microsoft·nvidia·cuda
fpcc23 天前
并行编程实战——CUDA编程的pipelines
c++·cuda
basketball61625 天前
AI Infra 硬件体系与编程模型:14. CUDA编程基础:事件与精确性能测量
人工智能·nvidia·cuda
kyle~25 天前
推理部署---CUDA 执行模型(SM、Block、Warp 与 SIMT)
人工智能·nvidia·cuda
June`25 天前
如何组织一个并行程序
开发语言·cuda
basketball61625 天前
AI Infra 硬件体系与编程模型:15. CUDA编程基础:混合精度计算
人工智能·nvidia·cuda
June`25 天前
CUDA执行模型深入刨析
c++·人工智能·cuda
June`1 个月前
CUDA程序效率如何计算以及工具如何使用
算法·cuda