2026-06-07
GPU 算子 Profiling 实战指南:测什么、怎么看、如何定位瓶颈
写完一个 GPU kernel,你怎么知道它「够不够快」?Profiling 不是跑一遍看个时间就完了——你需要知道测什么指标、怎么看数据、如何从数字反推瓶颈位置。本文是一份实用的 profiling 方法论。
一、Profiling 测什么?核心指标体系
1.1 延迟(Latency)
最基本的指标:kernel 执行一次要多久。
测量工具:
torch.cuda.Event → 毫秒级精度,适合 Triton kernel
Nsight Systems → 微秒级,能看到 kernel 之间的 overlap
Nsight Compute → 单 kernel 细粒度,cycle 级
注意:
- 必须 warmup(前几次 GPU 频率未稳定)
torch.cuda.synchronize()保证 kernel 真正跑完- 多测几次取中位数或 p99,不要只看第一次
1.2 吞吐量(Throughput)
延迟的反面:单位时间能处理多少数据。
吞吐量 = 数据量 / 延迟
例如:处理 16M 个 float32 的 ReLU,耗时 0.36 ms
吞吐量 = 16M / 0.36ms = 44.4B elements/s ≈ 178 GB/s
对 memory-bound kernel,用带宽利用率衡量:
带宽利用率 = 实际吞吐量 / 理论峰值带宽 × 100%
对 compute-bound kernel,用 FLOPS 利用率衡量:
FLOPS 利用率 = 实际 FLOPS / 理论峰值 FLOPS × 100%
1.3 硬件利用率指标
| 指标 | 含义 | 从哪里看 | 好/坏标准 |
|---|---|---|---|
| Occupancy | SM 中活跃 warp 数 / 最大 warp 数 | Nsight Compute | >50% 一般够用 |
| Register usage | 每个线程用了多少寄存器 | TRITON_PRINT_AUTOTUNING |
<128 安全,>200 必定 spill |
| Register spills | 溢出到 local memory 的寄存器数 | Nsight / PTX 中的 st.local |
0 是理想值 |
| Shared memory usage | 每个 block 分配了多少 shared memory | Autotune 输出 | <100 KB(留 occupancy 空间) |
| Memory throughput | 实际显存带宽使用率 | Nsight Compute | memory-bound kernel 应 >80% |
| Compute throughput | 实际算力使用率 | Nsight Compute | compute-bound kernel 应 >50% |
1.4 Stall 原因分析
这是 Nsight Compute 最有价值的功能:你的 SM 在等什么?
| Stall 原因 | 含义 | 常见根因 |
|---|---|---|
| Long Scoreboard | 等 global memory 加载 | 访存未合并、L1 miss 多 |
| Short Scoreboard | 等 shared memory 或 L1 | bank conflict、shared memory 争用 |
| Barrier | 等同步 | __syncthreads() 开销大 |
| Branch | warp divergence | if-else 分支导致串行 |
| Not Selected | SM 没有调度这个 warp | occupancy 太低 |
实战建议:跑 ncu --set full 然后直接看 "Warp State Statistics" 饼图,哪个颜色最大就先看哪个。
二、三层 Profiling 工具链
快速诊断(秒级) 深度分析(分钟级) 终极武器(小时级)
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ GPU Event Timer │ → │ TRITON_PRINT_ │ → │ Nsight Compute │
│ (延迟 + 吞吐量) │ │ AUTOTUNING=1 │ │ (每周期每条指令) │
│ │ │ (寄存器/shared mem) │ │ │
└─────────────────┘ └─────────────────────┘ └──────────────────┘
自己写,秒出结果 环境变量,0 代码改动 硬件级,信息最全
2.1 GPU Event Timer — 入门首选
import torch
def profile_kernel(kernel, args, warmup=10):
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
# Warmup
for _ in range(warmup):
kernel(*args)
torch.cuda.synchronize()
# Timed
start.record()
kernel(*args)
end.record()
torch.cuda.synchronize()
return start.elapsed_time(end) # milliseconds
什么时候够了? 对比两个实现谁快、验证优化有没有效果。
什么时候不够? 你想知道「为什么慢」——这时候上 Nsight。
2.2 TRITON_PRINT_AUTOTUNING — 零成本诊断
TRITON_PRINT_AUTOTUNING=1 python my_kernel.py
输出每个 config 的:
Triton autotuning: my_kernel (1024, 1024, 1024)
config 0: BLOCK_M=64, BLOCK_N=64, num_warps=4, num_stages=2
n_regs=48, n_spills=0, shared_mem=8192, occupancy=0.75
time=0.234 ms
config 1: BLOCK_M=128, BLOCK_N=128, num_warps=8, num_stages=3
n_regs=96, n_spills=4, shared_mem=16384, occupancy=0.50
time=0.189 ms
关键解读:
n_regs=96:正常。超过 128 要警惕,超过 200 必定影响性能n_spills=4:少量 spill 可接受。超过 20 要考虑降低寄存器压力occupancy=0.50:50% — 对 compute-bound kernel 够了,memory-bound 则偏低
2.3 Nsight Compute — 深度定位
# 快速看关键指标
ncu --set basic python my_kernel.py
# 全面分析(慢,信息全)
ncu --set full -o profile_report python my_kernel.py
# 只看 memory 相关
ncu --section MemoryWorkloadAnalysis python my_kernel.py
Nsight 报告中优先看的 5 件事:
- Kernel Duration — 确认是你关心的 kernel
- Memory Throughput — 如果是 memory-bound kernel,看占有率
- Compute Throughput — 如果是 compute-bound kernel,看 SM 利用率
- Warp State Statistics — 饼图告诉你 SM 在等什么
- Source View — 每条指令的 stall 原因,精准到行
三、如何从 Profiling 数据定位问题
场景 1:Kernel 比预期慢 30-50%
诊断流程:
1. GPU Timer → 确认确实慢(排除 warmup 问题)
2. TRITON_PRINT_AUTOTUNING → 看 n_regs / n_spills
├─ n_regs > 200 → 寄存器溢出!降低 BLOCK_SIZE 或 num_warps
└─ n_regs 正常 → 进入 3
3. Nsight Compute → 看 stall 原因
├─ Long Scoreboard > 60% → memory latency 主导
│ └─ 优化:增大 BLOCK_SIZE,增加 num_stages,检查 coalescing
├─ Short Scoreboard > 40% → shared memory 瓶颈
│ └─ 优化:padding 避免 bank conflict,减少 barrier
└─ Not Selected > 50% → occupancy 太低
└─ 优化:减少寄存器或 shared memory 用量
场景 2:Triton kernel 比 PyTorch 慢
先确认对比是否公平:
- 数据类型是否一致?(fp16 vs fp32 性能差 2-8 倍)
- PyTorch 可能用了 cuBLAS/cuDNN 的融合 kernel
- PyTorch 的
torch.compile可能会做算子融合
如果确实慢:
1. 看 TRITON_PRINT_AUTOTUNING → 选最优 config 了吗?
2. 看 PTX → 对比 PyTorch 版本的 PTX(用 ncu 抓)
3. 检查是否有不必要的 global memory 往返
场景 3:Memory-bound kernel 带宽利用率低
ReLU 预期:~500 GB/s(接近峰值)
实际: ~200 GB/s ← 问题
排查清单:
□ BLOCK_SIZE 是否够大?(至少 512,建议 1024-2048)
□ 数据是否连续?(确保没有 stride 访问)
□ 是否有隐式的 fp32→fp16 转换?(每次转换都多一次访存)
□ Grid 是否足够大?(SM 没用满——56 个 SM 至少需要 56 个 block)
场景 4:Matmul 的 FLOPS 利用率上不去
Matmul 4096×4096×4096:
理论 fp16 Tensor Core: ~178 TFLOPS
实际: ~60 TFLOPS ← 只有 34%
排查清单:
□ BLOCK_K 是否太小?(建议 32 或 64,太小则循环次数多)
□ BLOCK_M/BLOCK_N 是否够大?(让 Tensor Core 吃饱)
□ K 是否能整除 BLOCK_K?(不能的话最后一个迭代浪费算力)
□ num_stages 是否合适?(compute-bound kernel 只需 1-2 个 stage)
四、Profiling 的黄金法则
- 先判断 bound 类型,再优化 — memory-bound 和 compute-bound 的优化方向完全不同,别优化错了方向
- 不要过度相信理论峰值 — 实际能达到 70-80% 理论峰值就很好了
- Warmup 是必须的 — 不加 warmup 的 benchmark 是废数据
- 一次只改一个变量 — 同时改 BLOCK_SIZE 和 num_warps,你不知道哪个起了作用
- 记录每次 profiling — 维护一个 profiling log,不然过两天就忘了哪个 config 最快
五、Profiling Log 模板
保持一个简单的记录习惯:
Kernel: gelu_fp16
Date: 2026-06-07
GPU: RTX 4070 SUPER
| Config | Time (ms) | BW (GB/s) | n_regs | n_spills | Occupancy |
|---------------------------------|-----------|-----------|--------|----------|-----------|
| BLOCK=512, num_warps=4 | 0.42 | 480 | 32 | 0 | 0.75 |
| BLOCK=1024, num_warps=8 | 0.36 | 557 | 48 | 0 | 0.62 |
| BLOCK=2048, num_warps=16 | 0.38 | 530 | 72 | 2 | 0.50 |
结论: BLOCK=1024, num_warps=8 最优。更大 BLOCK 反而慢,因为 occupancy 下降。
本文是 Triton 系列的一部分。上一篇:Triton 性能调优方法论