引言
现代 GPU 已从固定功能的图形渲染管线演进为通用并行计算引擎(GPGPU)。CUDA、ROCm、Vulkan Compute、DirectCompute 等编程模型将 GPU 抽象为大规模多线程协处理器,而理解SIMT(单指令多线程)执行模型、线程层级结构、内存子系统是编写高性能 GPU 内核的基础。本文深入 GPU 硬件视角,解析波前/线程束调度、共享内存访问优化及同步原语实现。
1. GPU 线程层级与调度模型
在 CUDA 编程模型中,线程被组织为三维结构:
Thread:最小执行单元,拥有独立的程序计数器(部分 SIMT 实现中)、寄存器文件和局部内存。
Warp / Wavefront:SIMT 调度的基本单位。NVIDIA 的 Warp 固定 32 线程,AMD 的 Wavefront 固定 64 线程(CDNA/RDNA 架构可选择 32)。Warp 内所有线程共享同一指令地址,但通过活动掩码(active mask)控制每个线程是否执行。
Thread Block / Workgroup:多个 Warp 组成一个 Block,Block 内线程可通过共享内存(Shared Memory)通信,并通过屏障(__syncthreads() / barrier())同步。Block 是 GPU 上 SM/CU 的资源调度粒度。
Grid / Dispatch:多个 Block 构成一个 Grid,Grid 内 Block 完全独立,只能通过全局内存(Global Memory)通信。
2. SIMT 执行与分支发散
SIMT 的"单指令"特性意味着当 Warp 内各线程遇到条件分支时:
分支发散(Branch Divergence):若部分线程走 if 分支、其他走 else 分支,GPU 需先串行执行 if 掩码内线程,再执行 else 掩码内线程,两路径均完整执行但通过无效操作屏蔽另一路径。最坏情况(32 线程各自走不同分支)有效利用率降至 1/32。
Warps 级寄存器分配:SM/CU 的寄存器文件被均分为固定大小的分区,每个分区分配给活跃的 Warp。若某内核每线程使用 64 个 32 位寄存器,SM 寄存器文件 256KB,则每 SM 最多同时驻留 256KB / (64 * 4B * 32 Threads) = 32 个 Warp。寄存器使用量是决定Occupancy(SM 上Warp并发度)的核心因素之一。
指令双发射与延迟隐藏:NVIDIA GPU 的 SM 每个时钟周期可从两个 Warp 各发射一条指令(如一条 FP32 运算 + 一条内存加载),通过高并发 Warp 数量隐藏 DRAM 访问延迟(典型 400-800 个时钟周期的 DRAM 往返延迟需要至少 400 个并发周期/每线程延迟 Warp 才能隐藏)。
3. 共享内存架构与 Bank 冲突
存储体(Bank)结构:共享内存被组织为 32 个存储体(对应 Warp 宽度),每个 Bank 宽度 4 字节,32 个 Bank 可在同一时钟周期并行服务 32 个请求(全带宽)。若 Warp 内多个线程访问不同 Bank 的 4 字节字,只需一个时钟周期即可完成;若多个线程访问同一 Bank 的不同地址,则发生Bank 冲突(Bank Conflict),请求被串行化。
冲突分类:
- n-way Bank 冲突:表示运行该行需 n 个时钟周期
- 广播(Broadcast):多个线程访问同一 Bank 同一字,控制器可广播该字,无冲突
- 广播性访问:通过 __ldg() 只读数据缓存(RO Cache)发送广播类访问
消除冲突的策略:
1. 填充(Padding):将共享内存数组从 [32][32] 改为 [32][33],每行偏移一列消除列访问的 Bank 冲突
2. 对角索引:将行索引映射到 BankIndex((threadIdx.x + row) 2),避免连续线程访问同一 Bank
3. 转置加载:先按行加载到共享内存,再按列读取,由共享内存承担 Bank 冲突但避免全局内存非合并访问
4. 全局内存合并访问
全局内存访问效率取决于合并度(Coalescing)。当 Warp 内线程的访问满足合并条件时,GPU 可将多次访问合并为一次内存事务(32B、64B 或 128B 事务):
合并度计算:有效字节 / 总传输字节。若 Warp 访问 32 个不连续的 4 字节字分散在 8 个 128B 缓存行上,合并度 = 128 有效 / 1024 传输 = 12.5%,严重浪费带宽。
分区全局地址空间:GPU 的全局地址空间被划分到多个存储分区(Memory Partition),每个分区含独立的 DRAM 接口和 L2 Cache Slice。地址按交错(interleave)方式映射——连续 128B 行分配到不同存储分区,因此相邻线程应访问连续地址以最大化存储分区利用率(分区并发)。
L2 Cache 预取:NVIDIA A100 之后架构提供 L2 持久化缓存(L2 Persistent),允许应用程序将标记为持久化的数据锁定在 L2 Cache 中,避免被逐出,对随机小数据集性能稳定提升显著。
5. 栅栏与同步原语
__threadfence()(CUDA)/ mem_fence()(OpenCL):保证当前线程的内存操作在其他线程可见之前完成。仅处理内存可见性,不阻止重排序。
__syncthreads():Block 级别同步栅栏,确保共享内存操作在栅栏后对所有线程可见。其实现依赖 SM 内的硬件同步控制器和 Warp 调度器——等待调度的 Warp 被标记为阻塞直到所有 Block 线程到达栅栏点。
Cooperative Groups:CUDA 9+ 引入的灵活同步抽象,允许程序员定义比 Thread Block 更细粒度的同步范围(如 Grid-level this_grid.sync() 配合同步内核),以及跨 Block 级别的 Grid 同步。实现上通过隐式 IPC 机制(如 Kernel Launch 调度序列 + 全局 Device 信号量)完成。
6. Tensor Core 与结构化稀疏
现代 GPU(Volta 之后)包含专用的矩阵乘法累加单元。Tensor Core 每个时钟周期执行 4x4 矩阵乘累加(MMA),通过混合精度计算(FP16 输入、FP32 累积)实现显著吞吐提升。CUDA 通过 nvcuda::wmma API(或 mma.sync 内联 PTX)操作 Warp 级矩阵片段(Fragment)。
结构化稀疏(Structured Sparsity):A100 引入 2:4 稀疏模式,即每 4 个连续元素中恰好 2 个为零。稀疏矩阵通过压缩存储区(2倍压缩比)和专用稀疏 MMA 指令实现 2 倍理论加速,已广泛用于大模型推理和结构化剪枝优化。
总结
GPU 计算的优化本质是围绕内存层次结构和 SIMT Warp 执行模型的艺术:通过共享内存分块减少全局内存访问、通过 Bank 冲突消除保持共享内存带宽、通过布局优化实现合并访问避免带宽碎片。随着 Tensor Core 和张量内存加速器的集成,理解这些底层机制对于发挥现代 GPU 全部算力愈发重要。

发表评论 取消回复