以下是针对 2.2 Writing CUDA SIMT Kernels 和 2.2.1-2.2.2 子章节内容的详细知识点整理及配套练习题。
2.2. Writing CUDA SIMT Kernels (编写CUDA SIMT内核)
核心思想
- CUDA C++内核的编写方式与传统CPU代码类似
- 但GPU有独特特性可用于提升性能
- 理解线程调度、内存访问和执行流程有助于最大化资源利用率
2.2.1. Basics of SIMT (SIMT基础)
SIMT模型概述
| 概念 | 说明 |
|---|---|
| CUDA线程 | 从开发者视角看,是并行性的基本单位 |
| Warp(线程束) | GPU执行的基本单位,32个线程为一组 |
| SIMT | 单指令多线程(Single Instruction, Multiple Threads)模型 |
SIMT模型的关键特性
线程独立性
- 每个线程维护自己的状态(程序计数器、寄存器等)
- 每个线程可以有自己的控制流
- 从功能角度看,每个线程可以执行不同的代码路径
性能优化要点
- 避免线程束发散:同一warp内的线程应尽量减少执行不同代码路径的情况
- 线程束发散会导致串行化执行,降低并行效率
SIMT vs SIMD
| 特性 | SIMT (CUDA) | SIMD (CPU向量化) |
|---|---|---|
| 执行模型 | 多线程并行执行同一条指令 | 单指令操作多个数据元素 |
| 线程独立性 | 每个线程有独立状态和控制流 | 无独立线程概念 |
| 编程模型 | 标量编程,类似多线程 | 向量编程,需显式使用向量指令 |
2.2.2. Thread Hierarchy (线程层次结构)
层次结构概述
Grid (网格)
├── Thread Block 0 (线程块0)
│ ├── Thread 0
│ ├── Thread 1
│ └── ...
├── Thread Block 1
│ ├── Thread 0
│ ├── Thread 1
│ └── ...
└── ...
维度支持
- 网格:可以是1维、2维或3维
- 线程块:可以是1维、2维或3维
内建变量汇总
| 变量 | 类型 | 描述 | 设置时机 |
|---|---|---|---|
gridDim.x / .y / .z |
dim3 |
网格在x、y、z维度的大小 | 内核启动时设置 |
blockDim.x / .y / .z |
dim3 |
线程块在x、y、z维度的大小 | 内核启动时设置 |
blockIdx.x / .y / .z |
uint3 |
当前线程块在网格中的索引 | 执行时变化 |
threadIdx.x / .y / .z |
uint3 |
当前线程在线程块中的索引 | 执行时变化 |
多维索引的使用目的
多维线程块和网格只是为了方便,不影响性能
- 提供编程便利性,便于映射到问题域(如矩阵、图像、体数据)
- 线程块内的线程有可预测的线性化顺序
线程线性化顺序
存储顺序(C语言行优先)
- x维度变化最快(最内层)
- y维度次之(步长为
blockDim.x) - z维度最慢(步长为
blockDim.x * blockDim.y)
线性索引计算公式
1维线程块(最常用):
cpp
int tid = threadIdx.x + blockIdx.x * blockDim.x;
2维线程块:
cpp
int tid_x = threadIdx.x + blockIdx.x * blockDim.x;
int tid_y = threadIdx.y + blockIdx.y * blockDim.y;
int linear_index = tid_y * gridDim.x * blockDim.x + tid_x;
3维线程块:
cpp
int tid_x = threadIdx.x + blockIdx.x * blockDim.x;
int tid_y = threadIdx.y + blockIdx.y * blockDim.y;
int tid_z = threadIdx.z + blockIdx.z * blockDim.z;
int linear_index = tid_z * gridDim.x * blockDim.x * gridDim.y * blockDim.y
+ tid_y * gridDim.x * blockDim.x + tid_x;
线程到Warp的映射
- 线性化顺序影响线程如何被分配到warp中
- 连续线程索引 (threadIdx.x连续)会被分配到同一个warp
- 这很重要,因为:
- 同一warp内的线程执行同一条指令
- 连续线程通常访问连续内存,有利于内存合并访问
图9示例说明
图9展示了一个简单的2D网格,包含1D线程块的示例:
- 网格是2维的(有行和列)
- 每个线程块是1维的(只有x维度)
- 这种配置常用于处理2D数据(如矩阵),但每个块处理一维数据段
编程模式总结
计算全局唯一索引的模式
1D网格 + 1D块(最常见):
cpp
int global_idx = blockIdx.x * blockDim.x + threadIdx.x;
1D网格 + 2D块:
cpp
int global_idx = blockIdx.x * blockDim.x * blockDim.y
+ threadIdx.y * blockDim.x + threadIdx.x;
2D网格 + 2D块(适用于2D数据):
cpp
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int global_idx = y * width + x; // width是全局宽度
最佳实践总结
- 避免线程束发散:尽量让同一warp内的线程执行相同代码路径
- 合理使用多维索引:根据问题域选择合适维度,便于代码理解
- 利用线性化顺序:确保连续线程访问连续内存,实现合并访问
- 计算全局索引:使用内建变量组合计算每个线程的全局唯一ID
- 注意边界条件:处理数据大小不是块大小整数倍的情况
- 理解warp分配:threadIdx.x连续的线程在同一warp,影响内存访问模式
知识点巩固练习题
一、选择题
1. 关于SIMT模型,下列说法正确的是?
A. SIMT模型中所有线程必须执行完全相同的代码路径
B. SIMT模型中每个线程可以有自己的控制流,但线程束发散会影响性能
C. SIMT模型不允许线程有不同的状态
D. SIMT就是SIMD的另一种说法
2. 在CUDA中,线程束(Warp)的大小是多少?
A. 16个线程
B. 32个线程
C. 64个线程
D. 取决于GPU型号
3. 关于多维线程块和网格,下列说法正确的是?
A. 使用2D线程块比1D线程块性能更好
B. 多维只是为了编程方便,不影响性能
C. 必须使用与问题维度相同的维度
D. 3D网格比2D网格性能更好
4. 在线程块的线性化顺序中,哪个维度变化最快?
A. x维度
B. y维度
C. z维度
D. 取决于编译器
5. 如果一个线程块的大小是blockDim.x=16, blockDim.y=8,那么threadIdx.y的步长是多少?
A. 8
B. 16
C. 128
D. 1
6. 哪个内建变量用于获取线程块在网格中的索引?
A. threadIdx
B. blockIdx
C. blockDim
D. gridDim
7. 在1D网格和1D线程块的配置下,计算全局唯一索引的正确公式是?
A. threadIdx.x
B. blockIdx.x
C. threadIdx.x + blockIdx.x
D. blockIdx.x * blockDim.x + threadIdx.x
8. 关于线程束发散,下列说法正确的是?
A. 线程束发散可以提高并行性
B. 线程束发散会导致同一warp内的线程串行执行不同分支
C. 线程束发散无法避免
D. 线程束发散只影响内存访问
9. 如果一个线程块是2D的,blockDim.x=32, blockDim.y=4,那么一个warp(32线程)包含哪些线程?
A. 32个连续的threadIdx.x
B. 32个连续的threadIdx.y
C. 4行完整的x维度线程(每行32个)
D. 无法确定
10. 以下哪个场景最容易导致线程束发散?
A. 所有线程执行相同的指令
B. 根据threadIdx.x的值执行不同的分支
C. 所有线程访问连续的内存地址
D. 所有线程计算相同的表达式
二、填空题
**1. CUDA线程是并行性的 _____ 单位,而GPU执行的基本单位是 _____。
**2. SIMT模型允许每个线程维护自己的 _____ 和 _____。
**3. 为了提高性能,应尽量减少同一 _____ 内的线程执行不同的代码路径。
**4. 线程层次结构中,多个线程组成 _____ ,多个 _____ 组成网格。
5. 网格和线程块都可以是 _____、 ** 或 **** 维的。
6. 内建变量 _____ 用于获取线程块的大小,_____** 用于获取网格的大小。
7. 在C语言行优先存储中,线性化顺序是 _____ 维度变化最快, ** 维度次之,**** 维度最慢。
**8. 如果blockDim.x=16, blockDim.y=8, blockDim.z=4,那么threadIdx.z的步长是 _____。
**9. 连续线程索引(threadIdx.x连续)会被分配到同一个 _____ 中。
**10. 多维线程块和网格主要是为了 _____ 方便,不影响性能。
三、简答题
1. 解释SIMT模型的基本概念及其与SIMD的主要区别。
2. 什么是线程束发散?为什么应该避免它?
3. 描述CUDA的线程层次结构,并说明各层次的作用。
4. 列出所有与线程层次相关的CUDA内建变量,并说明每个变量的含义。
5. 解释为什么threadIdx.x连续的线程会被分配到同一个warp,这对性能有什么影响?
6. 给定一个2D网格(gridDim.x=4, gridDim.y=3)和2D线程块(blockDim.x=16, blockDim.y=8),写出计算全局唯一线程索引的公式。
7. 为什么说多维线程块和网格只是编程便利,不影响性能?
四、计算题
1. 给定以下内核启动配置:
cpp
dim3 grid(4, 2);
dim3 block(16, 8);
kernel<<<grid, block>>>();
计算:
- 总线程块数量
- 总线程数量
- 每个线程块内的线程数量
- 线程块(1,1)中线程(5,3)的线性化索引(块内)
- 整个网格中,线程块(2,0)中线程(10,4)的全局唯一线程索引(假设所有索引从0开始,按x优先线性化)
2. 编写一个内核函数,使用2D线程块处理2D矩阵,每个线程处理一个矩阵元素。要求:
- 矩阵大小为1024×1024
- 选择合适的网格和块维度
- 计算每个线程负责的全局行和列索引
- 添加边界检查
3. 分析以下代码,指出其中可能存在的性能问题:
cpp
__global__ void divergentKernel(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
if (idx % 2 == 0) {
data[idx] = idx * 2.0f;
} else {
data[idx] = idx * 3.0f;
}
}
}
参考答案
一、选择题答案
- B(SIMT允许独立控制流,但发散影响性能)
- B(warp大小固定为32线程)
- B(多维只是为了编程方便)
- A(x维度变化最快)
- B(threadIdx.y的步长是blockDim.x=16)
- B(blockIdx是块索引)
- D(经典公式:blockIdx.x * blockDim.x + threadIdx.x)
- B(发散导致串行执行不同分支)
- A(一个warp包含32个连续的threadIdx.x)
- B(根据threadIdx分支容易导致发散)
二、填空题答案
- 基本,线程束(warp)
- 状态,控制流
- 线程束(warp)
- 线程块,线程块
- 1维,2维,3维
blockDim,gridDim- x,y,z
blockDim.x * blockDim.y = 16 * 8 = 128- 线程束(warp)
- 编程
三、简答题答案要点
1. SIMT vs SIMD
- SIMT:单指令多线程,每个线程有独立状态和控制流,标量编程模型
- SIMD:单指令多数据,无独立线程概念,需显式向量化编程
- 主要区别:SIMT提供线程抽象,编程更接近多线程模型
2. 线程束发散
- 定义:同一warp内的线程因条件分支执行不同代码路径
- 影响:导致不同路径串行执行,降低并行效率
- 避免方法:重新组织数据或算法,使同一warp内线程行为一致
3. 线程层次结构
- 线程:最小执行单元,每个线程执行内核代码
- 线程块:线程集合,共享内存和同步,驻留同一SM
- 网格:线程块集合,覆盖整个问题域
- 作用:提供多层次并行,支持协作和资源共享
4. 内建变量
gridDim:网格维度(多少块)blockDim:块维度(每块多少线程)blockIdx:当前块索引threadIdx:当前线程在块内的索引
5. 连续threadIdx.x在同一warp的影响
- 原因:线程按x优先线性化分配到warp
- 影响:有利于内存合并访问,连续线程访问连续地址
- 优化:设计算法时利用这一特性
6. 2D全局索引公式
cpp
int blockId = blockIdx.y * gridDim.x + blockIdx.x;
int threadIdInBlock = threadIdx.y * blockDim.x + threadIdx.x;
int globalIdx = blockId * (blockDim.x * blockDim.y) + threadIdInBlock;
或者按行列:
cpp
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
int globalIdx = row * (gridDim.x * blockDim.x) + col;
7. 多维仅为编程便利的原因
- 硬件层面,所有线程最终线性化执行
- 多维映射便于问题域表达(矩阵、图像等)
- 性能取决于线程组织和内存访问模式,而非维度数量
四、计算题答案
1. 计算题答案
- 总线程块数量:4 × 2 = 8
- 总线程数量:8 × (16 × 8) = 8 × 128 = 1024
- 每块线程数:16 × 8 = 128
- 块(1,1)中线程(5,3)的块内线性索引:
threadIdx.y * blockDim.x + threadIdx.x = 3 × 16 + 5 = 53 - 块(2,0)中线程(10,4)的全局索引:
- 块ID =
blockIdx.y * gridDim.x + blockIdx.x = 0 × 4 + 2 = 2 - 块内线程ID =
4 × 16 + 10 = 74 - 每块线程数 = 128
- 全局ID =
2 × 128 + 74 = 330
- 块ID =
2. 2D矩阵处理内核
cpp
__global__ void matrixProcess(float* matrix, int width, int height) {
// 计算全局行列
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y * blockDim.y + threadIdx.y;
// 边界检查
if (row < height && col < width) {
int idx = row * width + col;
matrix[idx] = matrix[idx] * 2.0f; // 示例操作
}
}
// 启动配置
dim3 block(16, 16); // 每块16x16线程
dim3 grid((1024 + 15) / 16, (1024 + 15) / 16); // 64x64块
matrixProcess<<<grid, block>>>(d_matrix, 1024, 1024);
3. 性能问题分析
-
问题:根据
idx % 2进行分支,会导致warp发散 -
由于idx连续,每个warp中一半线程取模2为0,一半为1
-
改进方法:
cpp// 方法1:重新组织数据,使连续线程处理相同类型 // 方法2:使用三元运算符(可能被优化) // 方法3:使用算术方法统一计算 data[idx] = idx * (2.0f + (idx % 2));