计算优化
计算优化核心原则
- 辩证看待占用率:高占用率不等于 高性能
- 最大化指令级并行:隐藏延迟,保持计算单元忙碌
- 消除线程束分化:避免线程束内分支发散
- 利用线程束专用化(specialization):流水线并行优化
占用率优化
占用率定义
GPU 上的占用率指的是一个流多处理器(Multiprocessor)的活跃线程束数量与该流多处理器最大支持的线程束数量之比:
其中:
- 活跃线程束:流多处理器上同时被调度执行的线程束
- 流多处理器最大线程束数量:由线程束调度器决定(硬件固定)
- 活跃线程束数量:由内核硬件资源使用量限制
影响占用率的资源限制
| 资源类型 | 说明 | 影响 |
|---|---|---|
| 寄存器数量 | 每线程分配的寄存器数 | 寄存器过多 → 占用率下降 |
| 共享内存 | 每线程块使用的共享内存 | 共享内存过大 → 占用率下降 |
高占用率的优势
高占用率带来的性能优势:
- 确保总有足够的活跃线程束切换,隐藏长延迟操作
- 提供足够高的全局内存访问并发数
- 提高内存带宽利用率
- 确保每个流多处理器上有足够多线程,平衡负载
辩证看待占用率
并不是所有应用都需要高占用率!
结合屋顶模型(Roofline Model)分析:
| 应用类型 | 特征 | 占用率策略 | 理由 |
|---|---|---|---|
| 内存瓶颈 | 计算/访存比低 | 高占用率 | 需要足够高的访存并发数,将延迟瓶颈转化为带宽瓶颈 |
| 计算瓶颈 | 计算/访存比高 | 低占用率也可 | 充分的指令级并行带来高计算单元利用率,追求高占用率反而降低性能 |
案例分析:
// 计算瓶颈场景:矩阵乘法
// 高占用率策略(错误)
__shared__ float tileA[8][8]; // 小分块
__shared__ float tileB[8][8];
// 结果:计算/访存比降低,性能变差
// 低占用率策略(正确)
__shared__ float tileA[32][32]; // 大分块
__shared__ float tileB[32][32];
// 结果:计算/访存比高,计算单元利用率高,性能好
占用率调优
# 使用 Moore Perf Compute 分析占用率
mcu --metrics occupancy,sm_throughput -o report ./application
# 关键指标
# - sm__average_warp_execution_efficiency: 线程束有效执行比例
# - sm__active_warps.pct: 流多处理器上活跃线程束比例
消除线程束分化
线程束分化原理
在 MT GPU 中:
- 一个线程束包含 32 个线程
- 线程束共享同一个程序计数器(PC,Program Counter)
- 单指令多线程(SIMT)模型允许线程执行不同路径
- 线程束分化:线程束内线程执行不同分支路径
线程束分化的代价:
- GPU 串行执行各分支路径
- 为每个线程设置活跃掩码
- 非活跃线程的计算单元闲置
- 总执行时间 ≈ 各分支时间之和
产生线程束分化的场景
// ❌ 条件分支导致线程束分化
__global__ void divergentKernel(float* data, int threshold) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (data[tid] > threshold) { // 线程束内线程可能走不同分支
data[tid] = data[tid] * 2.0f; // 任务 A
} else {
data[tid] = data[tid] / 2.0f; // 任务 B
}
}
// 假设 threshold=100,线程束内 31 个线程 data>100,1 个线程 data<=100
// 结果:GPU 先执行任务 A(31 线程活跃),再执行任务 B(1 线程活跃)
// 计算单元利用率:50%(平均)
消除线程束分化的方法
方法 1:使用三元表达式
// ✅ 使用三元表达式(编译器优化为谓词指令)
__global__ void noDivergentKernel(float* data, int threshold) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
float val = data[tid];
data[tid] = (val > threshold) ? (val * 2.0f) : (val / 2.0f);
}
// 编译器生成谓词指令,避免显式分支
方法 2:重新设计算法
// ❌ 并行规约中的线程束分化(低效)
for (int stride = 1; stride < 8; stride *= 2) {
if (tid % (2 * stride) == 0) { // 线程束内分支分化
smem[tid] += smem[tid + stride];
}
__syncthreads();
}
// ✅ 优化后的并行规约(减少分化)
for (int stride = 4; stride > 0; stride /= 2) {
if (tid < stride) { // 线程束内线程执行相同路径
smem[tid] += smem[tid + stride];
}
__syncthreads();
}
// 前几轮无分化,仅最后一轮有分化
线程束分化检查清单
-
条件分支是否在线程束级别一致?
- 同一线程束的线程是否走相同分支
- 使用
threadIdx.x < threshold而非threadIdx.x % N == 0
-
是否可使用三元表达式?
- 简单条件判断使用
? :替代if-else
- 简单条件判断使用
-
循环边界是否对齐线程束?
- 确保循环次数为线程束大小的倍数
指令级并行(Instruction Level Parallelism)
指令级并行原理
指令级并行是指通过指令流水线技术,让多条指令重叠执行,提升处理器吞吐量。
GPU 流水线关键阶段:
取指 → 译码 → 发射 → 执行 → 写回
数据冒险:
- 写后读(RAW):真依赖,指令 B 依赖指令 A 的结果