第15章 性能调优实战 - 从理论到实践 (Performance Tuning in Practice - From Theory to Practice)
一、导读
15.1 本章学习目标
通过本章的学习,读者将能够:
掌握系统化的GPU性能调优方法论,从瓶颈识别到优化实施
熟练使用Nsight Compute和Nsight Systems等性能分析工具
理解Roofline模型及其在GPU性能分析中的应用
学会分析kernel的计算强度,判断是计算受限还是内存受限
掌握occupancy优化的技巧,理解何时应该追求高occupancy
了解指令级优化方法,包括寄存器使用和指令调度
能够设计有效的性能实验,隔离变量并量化优化效果
综合运用全书知识,解决实际的复杂性能问题
掌握性能调优中的常见陷阱和最佳实践
能够针对不同GPU架构进行可移植的性能优化
理解性能调优的经济价值和实际意义
掌握自动化性能调优工具的使用
15.2 为什么本章如此重要
在前面的章节中,我们学习了GPU架构、内存层次、并行模式等各种理论知识。然而,将这些知识转化为实际的性能提升需要系统化的调优方法和实践经验。性能调优不是盲目的尝试,而是基于数据驱动的分析和有针对性的优化。
首先,GPU性能的瓶颈可能来自多个方面:内存带宽、计算吞吐量、延迟、同步开销、资源竞争等。没有正确的分析工具和方法,程序员很难准确定位真正的瓶颈。错误的优化方向不仅浪费时间,还可能使性能变得更差。
其次,性能调优是一个迭代的过程。每次优化都可能改变瓶颈的位置,需要重新分析和调整策略。掌握系统化的调优流程可以帮助程序员高效地完成这一过程。
第三,现代GPU的复杂性使得直觉式的优化越来越不可靠。不同架构的GPU在指令延迟、带宽、缓存大小等方面有显著差异。依赖工具测量而非假设,是专业程序员的标志。
在实际项目中,性能调优往往是决定产品成败的关键因素。一个经过充分优化的kernel可能比初始版本快数十倍。这种提升可以直接转化为更低的成本、更好的用户体验或更强的竞争力。
本章将整合全书的知识,提供一个完整的性能调优框架。我们将介绍主流的分析工具,讲解如何解读性能数据,然后通过实际案例展示从瓶颈识别到优化实施的完整流程。
性能调优的经济价值:
在实际项目中,性能优化可以带来显著的经济效益:
降低成本:更高的性能意味着可以使用更少的硬件资源完成相同的工作
提高用户体验:实时应用(如游戏、视频处理)需要低延迟和高帧率
扩大应用范围:某些应用只有在达到一定性能阈值后才具有实用价值
节能减排:高效的代码消耗更少的能源,符合可持续发展理念
竞争优势:在同等硬件条件下,性能更好的产品更具市场竞争力
例如,在深度学习训练中,优化后的模型可以将训练时间从几周缩短到几天,大幅降低云计算成本。在科学模拟中,优化可以使原本需要数月才能完成的模拟在几天内完成。在实时渲染中,优化可以实现更高的帧率和更好的视觉效果。
性能调优的心理层面:
性能调优不仅是技术活动,也是一种思维训练。它培养了:
- 系统性思维:从整体角度分析问题
- 数据驱动决策:基于测量而非假设
- 耐心与毅力:调优往往是迭代的过程
- 创造力:寻找新颖的优化方法
15.3 核心问题引导
性能调优的标准流程是什么?如何避免常见的调优陷阱?
Nsight Compute和Nsight Systems各有什么特点?何时使用哪个工具?
什么是Roofline模型?如何使用它来分析kernel的性能瓶颈?
如何计算kernel的计算强度(Arithmetic Intensity)?
Occupancy与性能的关系是什么?高occupancy一定意味着高性能吗?
如何分析寄存器使用情况?寄存器溢出对性能有何影响?
指令级优化有哪些方法?编译器自动优化的能力如何?
如何设计公平的性能实验?需要注意哪些控制变量?
如何针对不同GPU架构进行可移植的优化?
自动化调优工具有哪些?它们的局限性是什么?
二、核心概念详解
15.1 性能调优方法论
系统化调优流程:
建立基准(Baseline):测量当前版本的性能,作为比较的基础
性能剖析(Profiling):使用工具收集详细的性能数据
瓶颈识别(Bottleneck Identification):分析数据,确定主要瓶颈
假设形成(Hypothesis Formation):基于分析提出优化假设
优化实施(Optimization):实施针对性的优化
验证测量(Verification):测量优化后的性能,确认效果
迭代(Iteration):回到步骤2,寻找下一个瓶颈
常见陷阱:
- 过早优化:在没有确定瓶颈之前进行优化,可能浪费精力
- 局部最优:优化某个子部分但忽略了整体瓶颈
- 忽视正确性:优化后必须验证结果的正确性
- 平台依赖:针对特定硬件的优化可能在其他硬件上表现不佳
- 微观优化陷阱:花费大量时间优化不重要的代码路径
- 忽视数据传输:只关注kernel执行时间,忘记主机-设备传输开销
调优优先级原则:
先优化算法复杂度,再优化实现细节
先优化热点代码(占大部分时间的部分),再优化次要部分
先优化容易实现的优化,再考虑复杂的优化
先优化对正确性影响小的部分,再优化关键路径
15.2 性能分析工具
Nsight Compute:
Nsight Compute是NVIDIA提供的kernel级别性能分析工具,提供详细的指标:
- 执行配置:Grid/Block大小、共享内存使用、寄存器使用
- 内存指标:L1/L2缓存命中率、DRAM利用率、合并访问效率
- 计算指标:SM吞吐量、指令混合、Tensor Core利用率
- Occupancy:活跃warp数量、限制因素分析
- 源码关联:将性能指标映射到源代码行
- 建议系统:提供优化建议
使用方法:
ncu --set full ./my_cuda_program
ncu --metrics lts__t_sectors_srcnode_lsu_sum,sm__sass_thread_inst_executed_op_fadd_pred_on.sum ./my_cuda_program关键指标解读:
sm__sass_thread_inst_executed: 执行的指令数lts__t_sectors: L2缓存访问dram__bytes: DRAM传输字节数sm__warps_active: 活跃warp数
Nsight Systems:
Nsight Systems是系统级别的分析工具,关注整个应用的执行时间线:
- 时间线视图:显示CPU线程、GPU stream、API调用的时间分布
- API追踪:记录所有CUDA API调用及其耗时
- 多GPU分析:显示多GPU系统中的负载平衡
- 系统指标:CPU利用率、内存使用、PCIe传输
- Python支持:可以分析PyTorch/TensorFlow应用
使用方法:
nsys profile ./my_cuda_program
nsys-ui # 图形界面查看结果nvprof(已弃用但仍可用):
早期的性能分析工具,功能较简单,但在某些场景下仍有用。
其他工具:
- CUPTI:CUDA Profiling Tools Interface,用于自定义 profiling
- Vampir:可视化性能分析工具
- Perf:Linux系统级性能分析工具
15.3 Roofline模型
Roofline模型的定义:
Roofline模型是一种可视化性能分析方法,将kernel的性能上限表示为计算强度和硬件特性的函数。
核心概念:
计算强度(Arithmetic Intensity):每字节内存传输的浮点运算次数(FLOPs/byte)
$$I = \frac{\text{Total FLOPs}}{\text{Total Bytes Transferred}}$$
峰值性能边界:
- 计算上限:峰值FLOPS(如19.5 TFLOPS for A100 FP32)
- 内存上限:峰值带宽 × 计算强度
Roofline曲线:取两个上限的最小值
$$Performance = \min(\text{Peak FLOPS}, \text{Peak Bandwidth} \times I)$$
分析方法:
将kernel的实际性能绘制在Roofline图上:
- 如果接近计算上限:计算受限(compute-bound)
- 如果接近内存上限:内存受限(memory-bound)
- 如果远低于两者:存在优化空间
示例:
对于A100 GPU:
- 峰值FP32性能:19.5 TFLOPS
- 峰值显存带宽:1.6 TB/s
转折点的计算强度:
$$I_{turnpoint} = \frac{19.5 \times 10^{12}}{1.6 \times 10^{12}} \approx 12.2 \text{ FLOPs/byte}$$
如果kernel的计算强度低于12.2,它是内存受限的;高于12.2,则是计算受限的。
多屋顶线:
现代GPU有多个执行单元,可以有多个roofline:
- FP32 roofline
- FP64 roofline
- Tensor Core roofline
- INT8 roofline
实际应用:
计算kernel的理论计算强度
测量实际性能
在Roofline图上绘制
分析距离roofline的差距
确定优化方向
15.4 Occupancy分析
Occupancy的定义:
Occupancy是活跃warp数量与SM最大支持warp数量的比值。
$$Occupancy = \frac{\text{Active Warps per SM}}{\text{Max Warps per SM}}$$
影响Occupancy的因素:
每Block线程数:更多的线程每Block意味着更少的Block可以驻留
每线程寄存器使用量:更多的寄存器限制了可以驻留的线程数量
每Block共享内存使用量:更多的共享内存限制了可以驻留的Block数量
Occupancy与性能的关系:
高Occupancy有助于隐藏内存延迟,因为当一些warp等待时,可以切换到其他warp。但高Occupancy不是高性能的充分条件:
- 对于计算受限的kernel,Occupancy的影响较小
- 有时降低Occupancy可以增加每线程可用的寄存器和共享内存,简化代码逻辑
- 过高的Occupancy可能导致缓存压力增加
- 对于延迟隐藏的kernel,需要足够的Occupancy
查询理论Occupancy:
使用CUDA Occupancy Calculator(Excel工具)或Nsight Compute可以计算理论最大Occupancy。
`cudaOccupancyMaxPotentialBlockSize` API:
int minGridSize, blockSize;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize,
myKernel, 0, 0);这个API可以根据kernel的资源需求推荐Block大小。
Occupancy优化策略:
调整Block大小以获得更好的Occupancy
减少寄存器使用(使用__launch_bounds__或--maxrregcount)
减少共享内存使用
使用寄存器 spill 到共享内存(手动)
15.5 寄存器使用分析
寄存器分配:
编译器负责将变量分配到物理寄存器。可以通过以下方式控制:
`__launch_bounds__`编译指示:
__global__ void __launch_bounds__(256, 4) myKernel(...) {
// Expected 256 threads per block, max 4 blocks per SM
}`maxrregcount`编译器选项:
nvcc --maxrregcount=32 kernel.cu寄存器溢出:
当寄存器不足时,编译器会将部分变量溢出到本地内存(全局内存的一部分)。这会导致:
- 大量的全局内存读写
- 性能急剧下降
- 增加的内存流量
检测寄存器溢出:
- 查看Nsight Compute的"Local Memory Throughput"指标
- 编译时使用
--ptxas-options=-v查看详细信息 - 观察
lcl(local memory)的使用情况
优化策略:
减少活变量数量:尽早释放不再需要的变量
重用变量:用同一个变量存储不同阶段的中间结果
使用共享内存:将频繁访问的数据放入共享内存
调整Block大小:较小的Block可以使用更多寄存器每线程
拆分kernel:将大kernel拆分为多个小kernel
15.6 指令级优化
指令混合:
GPU有不同的执行单元(INT、FP32、FP64、SFU、Tensor Core)。理想的指令混合可以充分利用所有单元。
查看指令混合:
- 使用Nsight Compute的"Instruction Statistics"部分
- 关注
inst_executed各类指令的数量
指令延迟隐藏:
某些指令(如除法、平方根、超越函数)有较高的延迟。编译器会尝试重新排序指令以隐藏延迟,但程序员也可以帮助:
// Instead of:
float a = x / y; // High latency
float b = a + 1; // Depends on a
// Consider:
float c = z * w; // Independent, can execute in parallel
float a = x / y;
float b = a + 1;快速数学函数:
使用__fdividef()代替/,使用__sinf()代替sinf()等,可以获得更快的速度但牺牲精度。
启用快速数学:
nvcc --use_fast_math kernel.cu循环展开:
手动展开循环可以减少分支开销:
// Before
for (int i = 0; i < 4; i++) {
sum += data[i];
}
// After
sum += data[0];
sum += data[1];
sum += data[2];
sum += data[3];编译器也可以自动展开循环,使用#pragma unroll提示。
谓词执行:
GPU使用谓词执行来处理条件分支。如果分支内的代码很短,可能不会产生实际的分支开销。
15.7 内存优化
合并访问:
确保相邻线程访问相邻的内存地址,以实现合并的全局内存访问。
// Good: coalesced access
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = value;
// Bad: strided access
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx * stride] = value;共享内存银行冲突:
共享内存分为32个银行。如果多个线程访问同一银行的不同地址,会发生银行冲突,导致串行化访问。
避免银行冲突的方法:
- 填充数组使相邻元素位于不同银行
- 重新排列数据访问模式
- 使用广播机制(多个线程访问同一地址是免费的)
缓存优化:
- L1缓存:每个SM私有,用于全局内存访问
- L2缓存:所有SM共享,用于全局内存和跨SM通信
- 使用
__ldg()intrinsic提示只读数据使用常量缓存
15.8 性能实验设计
控制变量:
在进行性能对比实验时,必须确保除了被测试的因素外,其他条件相同:
- 相同的输入数据
- 相同的GPU和驱动版本
- 相同的环境条件(温度、功耗限制等)
- 相同的编译选项
预热运行:
GPU在冷启动时可能运行在较低的时钟频率。在进行测量前,先执行几次预热运行:
// Warm-up
for (int i = 0; i < 5; i++) {
myKernel<<<gridSize, blockSize>>>(args);
}
cudaDeviceSynchronize();
// Actual measurement
cudaEventRecord(start);
for (int i = 0; i < N; i++) {
myKernel<<<gridSize, blockSize>>>(args);
}
cudaEventRecord(stop);统计显著性:
多次运行取平均值,计算标准差,确保结果具有统计显著性。
报告指标:
除了执行时间,还应报告:
- 吞吐量(如GB/s、GFLOPS)
- 加速比
- 效率(实际性能/峰值性能)
- 资源使用情况(寄存器、共享内存、Occupancy)
三、程序员的视角
3.1 在实际项目中的应用
深度学习框架优化:通过分析cuDNN kernel的性能,选择最佳的算法配置。
科学计算库开发:使用Roofline模型指导稀疏矩阵操作的优化方向。
实时渲染引擎:使用Nsight Systems分析帧时间,识别导致卡顿的瓶颈。
数据库加速:分析查询处理的各个阶段,优化最耗时的操作。
视频编码:优化运动估计和变换量化模块,提高编码速度。
3.2 常见陷阱与误区
过度依赖Occupancy:认为高Occupancy就一定高性能,忽视了其他因素。
忽视数据传输:只优化kernel执行时间,忘记了主机-设备传输的开销。
微观优化陷阱:花费大量时间优化不重要的代码路径。
平台特定优化:针对特定GPU型号的优化可能在其他型号上表现不佳。
忽视正确性验证:优化后未验证结果正确性,导致bug。
过早优化:在算法尚未稳定时就进行底层优化。
3.3 与其他层级的联系
与架构知识的联系:深入理解GPU架构是有效调优的基础。
与算法设计的联系:有时算法层面的改变比底层优化带来更大的收益。
与软件工程 practices的联系:性能调优应纳入开发流程,而不是事后补救。
与硬件的联系:了解目标硬件的特性可以指导优化方向。
四、代码实战
4.1 使用Nsight Compute分析kernel
#include <cuda_runtime.h>
#include <stdio.h>
__global__ void memoryBoundKernel(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
// Simple operation: low arithmetic intensity
data[idx] = data[idx] * 2.0f;
}
}
__global__ void computeBoundKernel(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
float val = data[idx];
// Heavy computation: high arithmetic intensity
for (int i = 0; i < 100; i++) {
val = sinf(val) * cosf(val) + sqrtf(fabs(val));
}
data[idx] = val;
}
}
int main() {
int N = 10000000;
size_t size = N * sizeof(float);
float *d_data;
cudaMalloc((void**)&d_data, size);
int blockSize = 256;
int gridSize = (N + blockSize - 1) / blockSize;
// Launch kernels
memoryBoundKernel<<<gridSize, blockSize>>>(d_data, N);
computeBoundKernel<<<gridSize, blockSize>>>(d_data, N);
cudaDeviceSynchronize();
printf("Kernels launched. Use Nsight Compute to analyze.\n");
cudaFree(d_data);
return 0;
}编译并运行:
nvcc -o analysis_demo analysis_demo.cu
ncu --set full ./analysis_demo4.2 Roofline分析示例
#include <cuda_runtime.h>
#include <stdio.h>
#define N 10000000
// Kernel with known arithmetic intensity
__global__ void stencilKernel(const float* input, float* output, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx > 0 && idx < N - 1) {
// 3-point stencil: 2 loads, 1 store, 2 FLOPs
// Arithmetic intensity ≈ 2 FLOPs / (12 bytes) ≈ 0.17 FLOPs/byte
output[idx] = (input[idx - 1] + input[idx + 1]) * 0.5f;
}
}
int main() {
float *h_input = (float*)malloc(N * sizeof(float));
float *h_output = (float*)malloc(N * sizeof(float));
for (int i = 0; i < N; i++) {
h_input[i] = (float)i;
}
float *d_input, *d_output;
cudaMalloc((void**)&d_input, N * sizeof(float));
cudaMalloc((void**)&d_output, N * sizeof(float));
cudaMemcpy(d_input, h_input, N * sizeof(float), cudaMemcpyHostToDevice);
int blockSize = 256;
int gridSize = (N + blockSize - 1) / blockSize;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaEventRecord(start);
for (int i = 0; i < 100; i++) {
stencilKernel<<<gridSize, blockSize>>>(d_input, d_output, N);
}
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms = 0;
cudaEventElapsedTime(&ms, start, stop);
// Calculate performance metrics
float totalBytes = N * sizeof(float) * 3 * 100; // Read 2, Write 1
float totalFlops = N * 2.0f * 100; // 2 FLOPs per element
float bandwidth = totalBytes / (ms / 1000.0f) / 1e9; // GB/s
float gflops = totalFlops / (ms / 1000.0f) / 1e9; // GFLOPS
printf("Execution time: %.3f ms\n", ms / 100);
printf("Effective bandwidth: %.2f GB/s\n", bandwidth);
printf("Effective GFLOPS: %.2f\n", gflops);
printf("Arithmetic Intensity: %.4f FLOPs/byte\n",
(float)(N * 2 * 100) / (N * sizeof(float) * 3 * 100));
cudaFree(d_input);
cudaFree(d_output);
free(h_input);
free(h_output);
cudaEventDestroy(start);
cudaEventDestroy(stop);
return 0;
}4.3 Occupancy调优示例
#include <cuda_runtime.h>
#include <stdio.h>
// Version 1: High register usage
__global__ void highRegKernel(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
// Many local variables
float a = data[idx];
float b = a * 2.0f;
float c = b + 1.0f;
float d = c * 3.0f;
float e = d - 2.0f;
float f = e * 4.0f;
float g = f + 5.0f;
float h = g * 6.0f;
data[idx] = h;
}
}
// Version 2: Reduced register usage through reuse
__global__ void lowRegKernel(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
float val = data[idx];
val = val * 2.0f;
val = val + 1.0f;
val = val * 3.0f;
val = val - 2.0f;
val = val * 4.0f;
val = val + 5.0f;
val = val * 6.0f;
data[idx] = val;
}
}
int main() {
int N = 10000000;
float *d_data;
cudaMalloc((void**)&d_data, N * sizeof(float));
int blockSize = 256;
int gridSize = (N + blockSize - 1) / blockSize;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// High register version
cudaEventRecord(start);
for (int i = 0; i < 50; i++) {
highRegKernel<<<gridSize, blockSize>>>(d_data, N);
}
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms1 = 0;
cudaEventElapsedTime(&ms1, start, stop);
// Low register version
cudaEventRecord(start);
for (int i = 0; i < 50; i++) {
lowRegKernel<<<gridSize, blockSize>>>(d_data, N);
}
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms2 = 0;
cudaEventElapsedTime(&ms2, start, stop);
printf("High reg kernel: %.3f ms\n", ms1 / 50);
printf("Low reg kernel: %.3f ms\n", ms2 / 50);
// Query occupancy
int minGridSize, blockSizeResult;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSizeResult,
highRegKernel, 0, 0);
printf("Recommended block size for highRegKernel: %d\n", blockSizeResult);
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSizeResult,
lowRegKernel, 0, 0);
printf("Recommended block size for lowRegKernel: %d\n", blockSizeResult);
cudaFree(d_data);
cudaEventDestroy(start);
cudaEventDestroy(stop);
return 0;
}4.4 合并访问优化示例
#include <cuda_runtime.h>
#include <stdio.h>
// Bad: Strided access
__global__ void stridedAccess(float* data, int N, int stride) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx * stride < N) {
data[idx * stride] *= 2.0f;
}
}
// Good: Coalesced access
__global__ void coalescedAccess(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
data[idx] *= 2.0f;
}
}
int main() {
int N = 10000000;
float *d_data;
cudaMalloc((void**)&d_data, N * sizeof(float));
int blockSize = 256;
int gridSize = (N + blockSize - 1) / blockSize;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// Strided access
cudaEventRecord(start);
for (int i = 0; i < 50; i++) {
stridedAccess<<<gridSize, blockSize>>>(d_data, N, 2);
}
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms1 = 0;
cudaEventElapsedTime(&ms1, start, stop);
// Coalesced access
cudaEventRecord(start);
for (int i = 0; i < 50; i++) {
coalescedAccess<<<gridSize, blockSize>>>(d_data, N);
}
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms2 = 0;
cudaEventElapsedTime(&ms2, start, stop);
printf("Strided access: %.3f ms\n", ms1 / 50);
printf("Coalesced access: %.3f ms\n", ms2 / 50);
cudaFree(d_data);
cudaEventDestroy(start);
cudaEventDestroy(stop);
return 0;
}五、深入思考
5.1 历史背景
GPU性能调优的方法论随着架构的演进而不断发展。早期主要依赖经验和试错,后来出现了系统的分析工具如nvprof。Nsight系列的推出标志着性能分析进入了精细化时代。
Roofline模型由Williams等人于2009年提出,最初用于CPU性能分析,后来被广泛应用于GPU。它提供了一个统一的框架来理解计算和内存的权衡。
5.2 前沿方向
机器学习辅助调优:使用ML模型预测最佳配置参数,减少人工调优的工作量。
自动化优化:编译器自动生成优化的kernel代码,如TVM、Halide等项目。
端到端分析:从应用到硬件的全栈性能分析,识别跨层级的瓶颈。
云原生调优:在云环境中进行分布式性能分析和优化。
自适应调优:运行时根据工作负载自动调整配置。
5.3 延伸阅读
NVIDIA Nsight Compute User Guide
Williams et al., "Roofline: An Insightful Visual Performance Model", 2009
"CUDA Best Practices Guide" - NVIDIA
Vasily Volkov, "Better Performance at Lower Occupancy", GTC 2010
"Programming Massively Parallel Processors" - Hwu and Kirk
六、实验笔记
实验1:Roofline模型验证
目的:验证Roofline模型对实际kernel性能的预测准确性。
步骤:
实现不同计算强度的kernel
测量实际性能
绘制Roofline图,比较预测与实际
预期:大部分kernel应接近Roofline边界,远离边界的kernel有优化空间。
实验2:Occupancy与性能关系研究
目的:研究occupancy对不同类别kernel性能的影响。
步骤:
实现内存受限和计算受限的kernel
通过调整Block大小和寄存器使用改变occupancy
测量不同occupancy下的性能
预期:内存受限kernel对occupancy更敏感,计算受限kernel受影响较小。
实验3:合并访问效果测量
目的:量化合并访问对性能的影响。
步骤:
实现合并和非合并访问的kernel
测量执行时间和内存吞吐量
比较差异
预期:合并访问应显著快于非合并访问,特别是在全局内存带宽受限的情况下。
七、本章小结
性能调优是将GPU潜力转化为实际收益的关键环节。本章介绍了系统化的调优方法论,强调基于数据的分析而非盲目尝试。
Nsight Compute和Nsight Systems是两大核心分析工具。前者提供kernel级别的详细指标,后者提供系统级别的时间线视图。结合使用这两个工具,可以全面理解应用的性能特征。
Roofline模型提供了一个直观的框架来分析kernel是计算受限还是内存受限。计算强度是判断的关键指标,它决定了kernel在Roofline图上的位置。
Occupancy是影响性能的重要因素,但不是唯一因素。高occupancy有助于隐藏延迟,但对于计算受限的kernel,其影响有限。有时降低occupancy以换取更多的寄存器或共享内存可能是更好的选择。
寄存器使用和指令级优化是更深层次的调优手段。通过减少寄存器压力和改善指令混合,可以进一步提升性能。
内存优化,特别是合并访问和共享内存银行冲突避免,对于内存受限的kernel至关重要。
最后,良好的实验设计是可靠调优结果的保证。控制变量、预热运行、统计分析都是必要的实践。
八、习题
Roofline计算:对于一个执行10^9次FLOP、传输4×10^9字节的kernel,计算其计算强度。在峰值19.5 TFLOPS、1.6 TB/s的GPU上,它是计算受限还是内存受限?
Nsight指标解读:如果在Nsight Compute中看到"L2 Cache Hit Rate"为30%,这意味着什么?如何改进?
Occupancy限制因素:如果一个kernel的occupancy只有25%,可能的限制因素有哪些?如何逐一排查?
寄存器溢出检测:如何通过Nsight Compute检测寄存器溢出?溢出对性能的影响有多大?
指令混合分析:分析一个kernel的指令混合,如果发现FP32单元利用率高而INT单元利用率低,说明了什么?
实验设计:设计一个实验来比较共享内存优化前后的性能差异。列出需要控制的变量。
性能建模:估算一个合并访问的全局内存读取和一个非合并访问的延迟差异。
调优优先级:给定三个瓶颈:内存带宽利用率50%、occupancy 40%、分支分歧率20%,应该优先优化哪个?
跨架构调优:为一个GPU优化的kernel在另一个GPU上性能下降,可能的原因是什么?如何编写可移植的优化代码?
综合调优:选择一个前面章节中的kernel,使用本章介绍的方法进行全面分析和优化。报告优化前后的性能对比和采取的措施。
缓存分析:如何使用Nsight Compute分析L1和L2缓存的命中率?什么样的命中率被认为是好的?
自动化调优:研究TVM或Ansor等自动化调优工具的工作原理。它们的优势和局限性是什么?