2155 字
11 分钟
CUDA 执行模型
概述
-
理解线程束执行的本质
- 掌握线程束(Warp)作为 GPU 基本调度单位的执行机制
- 理解线程束内 32 个线程的 SIMT(单指令多线程)执行模式
- 识别线程束分支发散对性能的影响
-
增大 GPU 的并行性
- 提高线程级并行度以隐藏内存延迟
- 平衡计算资源与内存带宽的利用率
- 通过增加活跃线程束数量提升 GPU 占用率(Occupancy)
-
掌握网格和线程块的启发式配置
- 根据 GPU 架构特性选择最优线程块大小
- 利用线程块维度与 Warp 大小的对齐关系
- 针对数据规模和 SM 数量调整网格尺寸
-
学习多种 CUDA 的性能指标和事件
- 使用 CUDA 事件(Event)精确测量核函数执行时间
- 利用 NVIDIA Visual Profiler / Nsight Compute 分析性能瓶颈
- 关注内存吞吐量、计算吞吐量、占用率等关键指标
-
了解动态并行与嵌套执行
- 掌握设备端启动核函数(Kernel Launch from Device)的机制
- 理解父子网格的同步与内存可见性规则
- 识别动态并行适用的递归算法场景与开销权衡
SIMT
GPU的并行结构
- SM(流式多处理器) 是GPU的基本计算单元,每个SM可同时执行数百个线程
- 一个GPU通常包含多个SM,因此整体可同时执行数千个线程
- 内核启动后,线程块被分配到可用的SM上执行
- 线程块一旦绑定到某个SM,其线程只能在该SM上运行
- 多个线程块可以共享同一个SM,具体取决于该SM的剩余资源
线程束(Warp)
- CUDA采用 SIMT(单指令多线程) 架构
- 每 32个线程 组成一个线程束(warp)
- 同一线程束内的所有线程同步执行相同指令
- 每个线程拥有独立的:
- 指令地址计数器
- 寄存器状态
- 执行路径(允许分支发散)
SIMT vs SIMD
| SIMD | SIMT | |
|---|---|---|
| 执行模式 | 向量元素强制同步执行 | 线程可独立执行 |
| 灵活性 | 低,一个同步组内必须一致 | 高,允许同一线程束内线程走不同分支 |
| 适用场景 | 数据并行,无分支 | 线程级并行,允许条件判断 |
为什么32是关键数字
- 32是SM以SIMD方式同时处理的工作粒度
- 线程数量按32对齐时,硬件资源利用率最高
- 非32倍数会造成线程束内部分线程空闲,降低效率
SIMT的三个核心特征(SIMD不具备)
- 每个线程有独立的指令地址计数器
- 每个线程有独立的寄存器状态
- 每个线程可以有独立的执行路径
为什么需要 Warp?
Block是软件层面的组织单位,Warp是硬件层面的执行单位。两者不在同一个维度。
| Block(线程块) | Warp(线程束) | |
|---|---|---|
| 层面 | 软件/编程模型 | 硬件/执行模型 |
| 作用 | 程序员组织线程的逻辑单元 | GPU实际调度和执行线程的物理单元 |
| 大小 | 程序员自定义(1~1024线程) | 固定32线程,由硬件决定 |
| 是否可选 | 必须显式定义 | 硬件自动划分,程序员通常不直接操控 |
为什么不把Warp做成1024线程?
1. 寄存器资源限制
每个线程需要独立的寄存器状态。假设每个线程用32个寄存器:
| Warp大小 | 单Warp所需寄存器 | 一个SM的寄存器总量(典型值) | 结论 |
|---|---|---|---|
| 32线程 | 1024个 | 65536个 | 一个SM可同时容纳64个Warp |
| 1024线程 | 32768个 | 65536个 | 一个SM只能同时容纳2个Warp |
Warp太大 → 同时活跃的Warp数量锐减 → 无法通过Warp切换隐藏内存延迟
GPU隐藏延迟的核心机制就是快速切换Warp:当一个Warp等待内存数据时,SM立刻换另一个Warp执行。如果SM上只有2个Warp,一个等内存时另一个也在等,SM就空转了。
2. 分支发散(Divergence)的代价
SIMT架构下,同Warp内线程走不同分支时必须串行执行:
if (threadIdx.x % 2 == 0) { A(); // 偶数线程执行} else { B(); // 奇数线程执行}- Warp大小32:一半线程等,一半执行,浪费50%执行周期
- Warp大小1024:极端情况下1023个线程等1个线程,浪费99.9%执行周期
Warp越大,分支发散的惩罚越重,并行效率越低。
3. 同步开销
Warp内所有线程共享指令流,但Warp之间需要显式同步。Warp越大:
- 同步屏障涉及线程越多,等待最慢线程的时间越长
- 共享内存竞争概率增加
4. 硬件复杂度和时钟频率
- 32路并行:控制逻辑相对简单,布线短,时钟频率可以做高
- 1024路并行:需要巨大的指令广播网络和结果收集网络,面积、功耗、延迟都会爆炸,时钟频率被迫降低
GPU追求高吞吐量,靠的是大量小单元高频运行,而不是少量大单元低频运行。
5. 内存访问合并(Coalescing)
Warp内线程的内存访问模式被硬件优化为合并成一次事务。32个线程的地址通常容易对齐和合并;1024个线程的地址范围太大,合并效率反而下降,内存带宽利用率降低。
为什么是32而不是其他数字?
| 因素 | 32的优势 |
|---|---|
| 二进制幂次 | 32 = 2⁵,地址计算、掩码操作硬件实现简单 |
| 与缓存行匹配 | 典型缓存行64~128字节,32个float/int刚好对齐 |
| 历史演进 | NVIDIA从早期架构沿用至今,软件生态已固化 |
| 经验验证 | 实际 workload 测试表明32在延迟隐藏和发散容忍间取得较好平衡 |
SM | Warp | Thread
Warp是硬件执行的原子单位,32是在寄存器开销、延迟隐藏能力、分支发散代价、硬件复杂度、内存合并效率之间权衡后的工程最优解,不是越大越好。

| 描述 | 含义 |
|---|---|
| ”一个线程块只能在一个SM上被调度” | 线程块是不可拆分的——一个Block的所有线程必须在同一个SM上执行,不能把一个Block拆到两个SM上 |
| ”一旦调度,保存在该SM上直到执行完成” | 线程块是不可迁移的——执行期间不会从SM A换到SM B |
| ”一个SM可以容纳多个线程块” | 一个SM的资源足够时,可以同时驻留多个Block |
| 概念 | 说明 |
|---|---|
| Block ↔ SM | 多对一(多个Block可以映射到同一个SM) |
| Block ↔ Block | 一个Block不能拆到多个SM(一对一绑定) |
块内用
__syncthreads()同步,块间靠内核拆分同步;Warp等待时不空转,SM立刻切到其他Warp执行,且切换不花代价,因为所有上下文早已静态分配在SM上。
两种 NVIDIA GPU 架构
Fermi
| 组件 | 规格 | 作用 |
|---|---|---|
| CUDA核心总数 | 512个 | 整数ALU + 浮点FPU,每个周期执行一条整数或浮点指令 |
| SM数量 | 16个 | 独立调度和执行单元,每个SM管理自己的Warp和资源 |
| 每SM CUDA核心 | 32个 | 单SM内的并行执行宽度 |
| 每SM Warp调度器 | 2个 | 双发射,每个周期可选两个就绪Warp发射指令 |
| 每SM LD/ST单元 | 16个 | 加载/存储操作,处理内存读写 |
| 每SM SFU | 4个 | 特殊函数单元(sin/cos、倒数、开方等) |
| 寄存器文件 | 32768 × 32位 | 所有活跃线程的寄存器存储 |
| 共享内存/L1缓存 | 64KB(可配置) | Block内线程共享数据,或与L1缓存动态划分 |
| 全局内存接口 | 6 × 384-bit GDDR5 | 总位宽2304-bit,高带宽访问显存 |
| 全局显存容量 | 最高6GB | 存储大规模数据集 |
| 主机接口 | PCIe总线 | CPU与GPU之间的数据传输通道 |
| GigaThread引擎 | 全局调度器 | 将线程块分配到各SM的Warp调度器 |


Kepler
| 特性 | Fermi | Kepler |
|---|---|---|
| 制程 | 40nm | 28nm |
| SM名称 | SM | SMX(下一代SM) |
| 每SM/SMX CUDA核心 | 32个 | 192个 |
| 每SM/SMX Warp调度器 | 2个(双发射) | 4个 |
| 每SM/SMX LD/ST单元 | 16个 | 32个 |
| 每SM/SMX SFU | 4个 | 32个 |
| 寄存器文件 | 32768 × 32位 | 65536 × 32位 |
| 共享内存/L1 | 64KB | 64KB(不变) |
| 新增 | 无 | 动态并行、Hyper-Q、GPU Boost |