CUDA GPU性能分析与跟踪
本文档提供了对CUDA应用程序进行性能分析和跟踪的全面指南,以识别性能瓶颈并优化GPU代码执行。
您可以在 https://github.com/eunomia-bpf/basic-cuda-tutorial 找到代码
目录
GPU性能分析简介
GPU性能分析是测量和分析GPU应用程序性能特征的过程。它帮助开发人员:
- 识别性能瓶颈
- 优化资源利用率
- 理解执行模式
- 验证优化决策
- 确保跨不同硬件的可扩展性
对于高性能CUDA应用程序,有效的性能分析至关重要,因为GPU架构的复杂性使得凭直觉进行优化是不够的。
性能分析工具
NVIDIA Nsight Systems
Nsight Systems是一个系统级性能分析工具,提供对CPU和GPU执行情况的洞察:
- 系统级跟踪:CPU、GPU、内存和I/O活动
- 时间线可视化:显示内核执行、内存传输和CPU活动
- API跟踪:捕获CUDA API调用及其持续时间
- 低开销:适用于生产代码分析
NVIDIA Nsight Compute
Nsight Compute是一个交互式CUDA应用程序内核分析器:
- 详细的内核指标:SM利用率、内存吞吐量、指令组合
- 引导分析:提供优化建议
- 屋顶线分析:显示相对于硬件限制的性能
- 内核比较:跨运行或硬件平台比较内核
NVIDIA Visual Profiler和nvprof
遗留工具(已弃用但对旧版CUDA仍然有用):
- nvprof:低开销的命令行分析器
- Visual Profiler:基于GUI的分析工具
- CUDA分析API:允许以编程方式访问分析数据
其他工具
- Compute Sanitizer:内存访问检查和竞争检测
- CUPTI:CUDA分析工具接口,用于自定义分析器
- PyTorch/TensorFlow分析器:针对深度学习的框架特定分析
关键性能指标
执行指标
- SM占用率:活动线程束与最大可能线程束的比率
- 更高的值通常能更好地隐藏延迟
-
目标:大多数应用程序>50%
-
线程束执行效率:执行期间活动线程的百分比
- 较低的值表示分支发散
-
目标:计算密集型内核>80%
-
指令吞吐量:
- 每时钟周期指令数(IPC)
- 算术密度(每字节操作数)
- 指令类型混合
内存指标
- 内存吞吐量:
- 全局内存读/写带宽
- 共享内存带宽
- L1/L2缓存命中率
-
目标:尽可能接近硬件峰值带宽
-
内存访问模式:
- 加载/存储效率
- 全局内存合并率
-
共享内存存储体冲突
-
数据传输:
- 主机-设备传输带宽
- PCIe利用率
- NVLink利用率(如果可用)
计算指标
- 计算利用率:
- SM活动
- Tensor/RT核心利用率(如果使用)
-
指令混合(FP32、FP64、INT等)
-
计算效率:
- 实现与理论FLOPS比较
- 资源限制(计算受限vs内存受限)
- 屋顶线模型位置
性能分析方法论
CUDA应用程序性能分析的结构化方法:
1. 初步评估
- 从高级系统分析开始(Nsight Systems)
- 识别CPU、GPU和数据传输之间的时间分布
- 寻找明显的瓶颈,如过度同步或传输
2. 内核分析
- 分析单个内核(Nsight Compute)
- 识别最耗时的内核
- 收集这些内核的关键指标
3. 瓶颈识别
- 确定内核是计算受限还是内存受限
- 使用屋顶线模型理解性能限制因素
- 检查特定低效情况(分歧、非合并访问)
4. 引导优化
- 首先解决最显著的瓶颈
- 一次进行一个变更并测量影响
- 比较前后分析结果以验证改进
5. 迭代改进
- 对下一个瓶颈重复该过程
- 定期重新分析整个应用程序
- 继续优化直到达到性能目标
常见性能瓶颈
内存相关问题
- 非合并内存访问:
- 症状:全局内存加载/存储效率低
-
解决方案:重组数据布局或访问模式
-
共享内存存储体冲突:
- 症状:共享内存带宽低
-
解决方案:调整填充或访问模式
-
过度全局内存访问:
- 症状:高内存依赖性
- 解决方案:通过共享内存或寄存器增加数据重用
执行相关问题
- 线程束分歧:
- 症状:线程束执行效率低
-
解决方案:重组算法以最小化分歧路径
-
低占用率:
- 症状:SM占用率低于50%
-
解决方案:减少寄存器/共享内存使用或调整块大小
-
内核启动开销:
- 症状:许多小型、短时间的内核
- 解决方案:内核融合或持久内核
系统级问题
- 过度主机-设备传输:
- 症状:PCIe利用率高,传输操作多
-
解决方案:批量传输,使用固定内存或统一内存
-
CPU-GPU同步:
- 症状:内核之间GPU空闲期
-
解决方案:使用CUDA流,异步操作
-
GPU资源未充分利用:
- 症状:总体GPU利用率低
- 解决方案:并发内核,流,或增加问题规模
跟踪技术
跟踪提供应用程序执行的时间线视图:
CUDA事件
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaEventRecord(start);
myKernel<<<grid, block>>>(data);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float milliseconds = 0;
cudaEventElapsedTime(&milliseconds, start, stop);
printf("Kernel execution time: %f ms\n", milliseconds);
NVTX标记和范围
NVIDIA工具扩展(NVTX)允许自定义注释:
#include <nvtx3/nvToolsExt.h>
// 标记一个瞬时事件
nvtxMark("Interesting point");
// 开始一个范围
nvtxRangePushA("Data preparation");
// ... 代码 ...
nvtxRangePop(); // 结束范围
// 彩色范围以提高可见性
nvtxEventAttributes_t eventAttrib = {0};
eventAttrib.version = NVTX_VERSION;
eventAttrib.size = NVTX_EVENT_ATTRIB_STRUCT_SIZE;
eventAttrib.colorType = NVTX_COLOR_ARGB;
eventAttrib.color = 0xFF00FF00; // 绿色
eventAttrib.messageType = NVTX_MESSAGE_TYPE_ASCII;
eventAttrib.message.ascii = "Kernel Execution";
nvtxRangePushEx(&eventAttrib);
myKernel<<<grid, block>>>(data);
nvtxRangePop();
使用CUPTI进行编程分析
CUDA分析工具接口(CUPTI)使程序能够访问分析数据:
// 简化的CUPTI使用示例
#include <cupti.h>
void CUPTIAPI callbackHandler(void *userdata, CUpti_CallbackDomain domain,
CUpti_CallbackId cbid, const void *cbInfo) {
// 处理回调
}
// 初始化CUPTI并注册回调
CUpti_SubscriberHandle subscriber;
cuptiSubscribe(&subscriber, callbackHandler, NULL);
cuptiEnableCallback(1, subscriber, CUPTI_CB_DOMAIN_RUNTIME_API,
CUPTI_RUNTIME_TRACE_CBID_cudaLaunch_v3020);
示例应用
附带的basic08.cu
演示了:
- 基本内核计时:使用CUDA事件
- NVTX注释:添加标记和范围
- 内存传输分析:分析主机-设备传输
- 内核优化:比较不同的实现策略
- 解释分析数据:做出优化决策
关键代码部分
基本内核计时:
__global__ void computeKernel(float *input, float *output, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
float x = input[idx];
// 执行计算
float result = x * x + x + 1.0f;
output[idx] = result;
}
}
void timeKernel() {
// 分配内存
float *d_input, *d_output;
cudaMalloc(&d_input, SIZE * sizeof(float));
cudaMalloc(&d_output, SIZE * sizeof(float));
// 初始化数据
float *h_input = new float[SIZE];
for (int i = 0; i < SIZE; i++) h_input[i] = i;
cudaMemcpy(d_input, h_input, SIZE * sizeof(float), cudaMemcpyHostToDevice);
// 使用事件计时
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// 预热运行
computeKernel<<<(SIZE + 255) / 256, 256>>>(d_input, d_output, SIZE);
// 计时运行
cudaEventRecord(start);
computeKernel<<<(SIZE + 255) / 256, 256>>>(d_input, d_output, SIZE);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float milliseconds = 0;
cudaEventElapsedTime(&milliseconds, start, stop);
printf("Kernel execution time: %f ms\n", milliseconds);
// 清理
delete[] h_input;
cudaFree(d_input);
cudaFree(d_output);
}
最佳实践
性能分析工作流
- 从高级分析开始,然后再深入细节
- 为重要内核建立基准
- 在开发过程中定期分析,而不仅仅是在结束时
- 尽可能自动化分析以进行回归测试
- 跨硬件比较以确保可移植性
工具选择
- Nsight Systems用于系统级分析和时间线
- Nsight Compute用于详细的内核指标
- NVTX标记用于自定义注释
- CUDA事件用于轻量级时间测量
优化方法
- 关注热点:首先解决最耗时的操作
- 使用屋顶线分析:了解理论限制
- 平衡努力:不要过度优化不太关键的部分
- 考虑权衡:有时可读性 > 微小的性能提升
- 记录见解:记录性能分析发现以供将来参考