第2节:线程组织与索引计算【CUDA高性能编程实战‑板块一】
📚 专栏:《CUDA 高性能编程实战:从 Kernel 到 FlashAttention》
✨ 本篇为板块一第2节,详解Grid/Block层级线程架构、三维索引、全局唯一线程ID计算,配套完整可运行代码示例
🎯 阅读目标:掌握任意维度下线程索引推导,能独立实现一维、二维数据并行读写
学习目标
学完本节你将掌握:
- 理解 Grid、Block、Thread 三层层级架构逻辑
- 分清
gridIdx/blockIdx/threadIdx三组内置变量含义 - 熟练计算一维、二维场景下全局唯一线程ID
- 写出通用索引计算模板,避免数组越界访问
- 理解
dim3三维启动参数的使用规则
1. CUDA三层线程架构
GPU并行执行单元分为三层嵌套结构,从上至下:
- Grid(网格):一次Kernel启动的全部线程集合,由多个Block组成
- Block(线程块):一组共享片上共享内存的线程,单个Block最大1024线程
- Thread(线程):最小执行单元,对应SIMT流水线单条执行通道
层级关系示意图:
Grid ├─ Block(0,0) │ ├─ Thread(0,0,0) │ ├─ Thread(1,0,0) │ └─ ... ├─ Block(1,0) │ ├─ Thread(0,0,0) │ └─ ... └─ ...核心特性:
- 同一个Block内线程可同步、访问共享内存;不同Block线程无通信机制
- Grid、Block均支持三维定义:x/y/z,日常一维计算只用x维度
2. 内置索引变量(只读)
CUDA自动为每个线程分配三组内置dim3类型索引:
| 变量名 | 作用域 | 含义 |
|---|---|---|
| gridDim | 全局 | Grid整体尺寸,gridDim.x=Block总数 |
| blockDim | 单个Block | 每个Block内线程总数,blockDim.x=单块线程数 |
| blockIdx | Block内所有线程 | 当前线程所在Block编号 |
| threadIdx | 单个线程 | 当前线程在Block内部的局部编号 |
一维场景索引逻辑(最常用)
一维下仅使用x分量,全局唯一线程ID计算公式:
int global_tid = blockIdx.x * blockDim.x + threadIdx.x;公式拆解:
blockIdx.x * blockDim.x:当前Block前面所有Block的线程总量+ threadIdx.x:叠加当前Block内部局部偏移
3. dim3 三维启动参数
dim3是CUDA内置三维结构体,默认未赋值维度自动填充为1。
两种启动写法等价:
// 写法1:纯int一维启动 kernel<<<32, 128>>>(); // 写法2:dim3标准三维启动 dim3 grid_size(32, 1, 1); dim3 block_size(128, 1, 1); kernel<<<grid_size, block_size>>>();二维矩阵计算标准dim3示例:
// 二维场景:每行一个Block,每个Block 256线程 dim3 grid(rows, 1); dim3 block(256, 1);4. 完整代码示例1:一维数组索引遍历
#include <cstdio> __global__ void indexTestKernel(int data_len) { // 计算全局唯一线程ID int tid = blockIdx.x * blockDim.x + threadIdx.x; // 边界判断:防止线程超出数组长度,避免越界 if (tid < data_len) { printf("Block:%d Thread:%d GlobalTid:%d\n", blockIdx.x, threadIdx.x, tid); } } int main() { int data_size = 20; // 启动配置:4个Block,每个Block 8线程,总32线程 indexTestKernel<<<4, 8>>>(data_size); cudaDeviceSynchronize(); cudaError_t err = cudaGetLastError(); if (err != cudaSuccess) { printf("Kernel启动异常:%s\n", cudaGetErrorString(err)); return -1; } return 0; }编译运行指令:
nvcc thread_index.cu -o thread_index ./thread_index5. 二维矩阵索引计算
图像处理、矩阵运算常用二维Grid/Block,全局坐标推导:
// 当前像素在矩阵中的x、y坐标 int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; // 一维展平数组下标(行优先存储) int global_idx = y * matrix_width + x;二维Kernel完整示例片段:
__global__ void matrixKernel(float* mat, int width, int height) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; if (x < width && y < height) { int idx = y * width + x; mat[idx] = x + y * 0.5f; } }6. 索引避坑核心要点
- 必须加边界判断
if(tid < length)
总线程数 =gridDim.x * blockDim.x,通常会大于数据长度,多余线程不处理数据,不加判断会数组越界、内存报错。 - Block单块线程上限1024
blockDim.x * blockDim.y * blockDim.z ≤ 1024,超过直接启动失败。 - 索引变量只读,不可修改
blockIdx.x = 5;这类代码编译报错。 - 全局ID公式固定不变
一维计算统一使用blockIdx.x * blockDim.x + threadIdx.x,不要颠倒乘数顺序。
7. 课后练习
- 修改一维示例启动参数为
<<<5, 6>>>,手动计算总线程数,观察输出的最大global_tid。 - 改写代码实现二维512×512矩阵遍历,dim3设置为
dim3 block(16,16),推导grid尺寸。 - 移除边界判断语句,传入较小data_len,观察程序是否出现内存错误。
- 尝试将blockDim设置为1025,查看Kernel启动报错信息,巩固Block线程限制知识点。
🔗专栏上下篇
- 上一篇:第1节:CUDA 编程模型与核函数【CUDA高性能编程实战‑板块一】
- 下一篇:第3节:SIMT与Warp基础【CUDA高性能编程实战‑板块一】
💡提示:所有代码复制后可直接nvcc编译运行,建议手动修改启动参数直观感受线程索引变化。