性能模型与工具
性能优化不是“换一种更高级的并行 API”,而是提出瓶颈假设,再用数据证伪。先保证正确,再建立可重复基线。
1. 一个最小实验协议
- 固定输入、构建模式、线程数、设备和正确性标准;
- 区分冷启动与稳态,多轮运行并报告中位数/分位数;
- 用墙钟时间衡量用户真正等待的路径;
- 用 profiler 解释时间花在哪里;
- 一次只改一个主要变量,并保留基线。
调试构建、日志输出、首次 JIT、动态频率和后台负载都可能污染结论。
2. Speedup 与效率
speedup S(p) = T(1) / T(p)
efficiency E(p)= S(p) / p8 个核心加速 6 倍,效率为 75%。效率下降可能来自串行部分、同步、内存带宽、通信和负载不均。
3. Amdahl 与 Gustafson
Amdahl 定律回答固定总工作量的强扩展上限:不可并行部分最终占主导。Gustafson 的视角是资源增加时扩大问题规模,说明并行机器可以在相近时间处理更多工作。二者前提不同,并不矛盾。
4. Roofline 心智模型
attainable performance <= min(
peak compute,
memory bandwidth * arithmetic intensity)Arithmetic intensity(算术强度)约等于每搬运一个字节执行多少运算。
performance
^ compute roof
| _____________
| /
| / bandwidth roof
+----------------------------> arithmetic intensity靠左的低算术强度 Kernel 多为带宽受限;靠右后才可能计算受限。提高算术强度通常依赖数据复用、分块和融合,而不仅是换指令。
5. 强扩展与弱扩展
- 强扩展:固定总问题规模,增加核心/节点,观察时间下降;
- 弱扩展:每个资源保持近似固定工作量,资源和总规模一起增长。
必须同时报告问题规模、资源数、通信量和基准版本,否则“扩展性很好”没有明确含义。
6. 工具按问题选择
| 想回答的问题 | 工具类别 |
|---|---|
| CPU 时间花在哪个函数 | sampling profiler,例如 perf、VTune 等 |
| Cache/分支/指令表现 | hardware counters |
| 是否向量化 | 编译器优化报告 + 反汇编 |
| 线程等待与负载均衡 | timeline/thread profiler |
| CUDA Kernel 与传输 | Nsight Systems / Nsight Compute |
| MPI 通信与等待 | MPI profiler / trace 工具 |
工具名不是结论。采样显示热点后,还要结合算法、调用次数、数据量和硬件计数器解释原因。
7. 性能优化顺序
correctness -> algorithm -> eliminate needless work/data movement
-> locality -> parallelism -> vectorization
-> low-level tuning -> remeasure end-to-end先把 O(n²) 算法换成 O(n log n),通常比微调一条 SIMD 指令价值更大。并行化错误算法只会更快地产生错误结果。
深入原理与工程实践
前面的内容负责建立统一心智模型;下面把同一主题继续拆到执行过程、代码、性能代价与工程判断。
<!-- migrated-deep-dive:start -->
完整迁入:原性能分析工具全文
性能分析工具——找到真正的瓶颈 / Performance Analysis Tools for Finding Real Bottlenecks
📅 创建时间:2026-06-02 🏷️ 标签:#HPC #性能分析 #nsys #ncu #vtune #PyTorchProfiler 📚 前置知识:[[06-cuda-optimization]](CUDA 优化) [[10-dl-training-optimization]](训练优化) 📚 相关知识:[[05-cuda-kernel-and-memory]](内存管理)
先抓住直觉
Profiler 像医院检查:先判断问题发生在整条训练流水线、某个 Kernel,还是内存分配,再选择对应工具。指标不是分数;“越高越好”通常不成立,关键是它能否解释实际耗时。
- 必须理解:先建立基线;从总时间逐层下钻;一次只验证一个假设。
- 用到再查:
nsys、ncu和 PyTorch Profiler 的命令参数。 - 最小流程:复现 → 测量 → 定位最大耗时 → 提出假设 → 修改 → 用同一基线复测。
场景:优化了半天,为什么还是没快?
┌─────────────────────────────────────────────────────────────┐
│ │
│ 你做了一堆优化: │
│ ✅ 合并内存访问 │
│ ✅ 使用共享内存 │
│ ✅ 优化了 block 大小 │
│ │
│ 自测结果: │
│ 优化前:15 GFLOPS │
│ 优化后:18 GFLOPS │
│ │
│ 提升 20%,但离 A100 的 156 TFLOPS 还差很远! │
│ │
│ 瓶颈到底在哪里? │
│ → 盲目优化 = 浪费时间 │
│ → 必须用工具定位瓶颈! │
│ │
└─────────────────────────────────────────────────────────────┘第1节:性能分析工具全景
┌─────────────────────────────────────────────────────────────┐
│ NVIDIA 性能分析工具 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Nsight Systems(nsys): │
│ 定位:系统级 timeline 分析 │
│ 看什么:CPU-GPU 传输时间、kernel 执行顺序、并行度 │
│ 类比:航空调度中心,看整个机场的运行 │
│ │
│ Nsight Compute(ncu): │
│ 定位:kernel 级别性能分析 │
│ 看什么:SM 利用率、内存带宽、warp 效率、瓶颈类型 │
│ 类比:单架飞机黑匣子,看具体故障原因 │
│ │
│ PyTorch Profiler: │
│ 定位:深度学习训练性能分析 │
│ 看什么:每层的耗时、CPU-GPU 传输、内存使用 │
│ │
│ VTune(Intel): │
│ 定位:CPU 性能分析 │
│ 看什么:CPU 利用率、OpenMP 效率、SIMD 利用率 │
│ │
└─────────────────────────────────────────────────────────────┘第2节:Nsight Systems——系统级 Timeline
基本用法
### 抓取性能数据
nsys profile --trace=cuda,nvtx,osrt \
--output=my_profile \
./my_cuda_app
### 生成 .qdrep 文件
### 用 Nsight Systems GUI 打开查看
### 或者命令行快速查看
nsys stats my_profile.qdrep输出解读
┌─────────────────────────────────────────────────────────────┐
│ nsys timeline 输出示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ GPU Core: │
│ ┌────────┬────────┬────────┬────────┬────────┐ │
│ │Kernel A│Kernel B│Kernel A│Kernel C│ Idle │ │
│ └────────┴────────┴────────┴────────┴────────┘ │
│ 0ms 5ms 10ms 15ms 20ms 25ms │
│ │
│ 观察:Kernel A 和 B 顺序执行,没有并行! │
│ → 可以用 CUDA Stream 并行化 │
│ │
│ GPU Utilization: 45% │
│ CPU-GPU Memcpy: 30% │
│ GPU Kernel: 45% │
│ Idle: 25% │
│ │
└─────────────────────────────────────────────────────────────┘常见问题的 timeline 特征
┌─────────────────────────────────────────────────────────────┐
│ 典型问题识别 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题1:CPU-GPU 传输瓶颈 │
│ Timeline: [H2D][GPU][D2H][H2D][GPU][D2H] │
│ 解决:用 CUDA Stream 重叠传输和计算 │
│ │
│ 问题2:GPU 空闲等待 │
│ Timeline: [GPU-Kernel][CPU-Preprocess][GPU-Kernel] │
│ 解决:Double Buffering,CPU 和 GPU 并行 │
│ │
│ 问题3:kernel 顺序执行 │
│ Timeline: [Kernel A][ Kernel B ] │
│ 解决:用多个 CUDA Stream 并行 │
│ │
│ 问题4:频繁的小 kernel 调用 │
│ Timeline: [K1][K2][K3][K4][K5][K6][K7][K8] │
│ 解决:合并多个小 kernel 为一个 │
│ │
└─────────────────────────────────────────────────────────────┘第3节:Nsight Compute——Kernel 级分析
基本用法
### 抓取 kernel 级别数据
ncu --set full \
--output my_kernel_profile \
./my_cuda_app
### 或者针对特定 kernel
ncu --set full \
--kernel-name "matrix_mul_kernel" \
--output my_kernel_profile \
./my_cuda_app输出指标解读
┌─────────────────────────────────────────────────────────────┐
│ ncu 输出指标解读 │
├─────────────────────────────────────────────────────────────┤
│ │
│ SM Utilization: │
│ 值:75% │
│ 含义:75% 的时间 SM 在执行有效指令 │
│ 低原因:分支分化、Warp 等待内存、Occupancy 低 │
│ │
│ Memory [GB/s]: │
│ Achieved:1800 GB/s │
│ 说明:实际内存带宽使用 │
│ 对比 HBM2e 理论:2000 GB/s → 利用率 90% │
│ │
│ SM Active Cycles: │
│ 值:high │
│ 含义:计算单元利用率高 │
│ │
│ L1/TEX Cache Hit Rate: │
│ 值:60% │
│ 含义:40% 的内存访问走了全局内存(慢) │
│ 解决:增加共享内存使用 │
│ │
│ Warp Occupancy: │
│ 值:48 warps/SM │
│ 含义:每 SM 平均 48 个 warp 在调度 │
│ 对比最大 64 warps/SM → Occupancy 75% │
│ │
└─────────────────────────────────────────────────────────────┘瓶颈定位流程
┌─────────────────────────────────────────────────────────────┐
│ ncu 瓶颈定位流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 查看 SM Utilization │
│ ├─ 低(< 50%):→ Step 2 │
│ └─ 高(> 80%):→ Step 3 │
│ │
│ Step 2: 查看 Warp Execution Efficiency │
│ ├─ 低(< 95%):分支分化问题 │
│ └─ 高(> 95%):→ Occupancy 问题 │
│ │
│ Step 3: 查看 MemorySOL / ComputeSOL │
│ ├─ MemorySOL < 50%:内存带宽瓶颈 │
│ └─ ComputeSOL < 50%:计算单元瓶颈 │
│ │
└─────────────────────────────────────────────────────────────┘第4节:PyTorch Profiler——训练性能分析
基本用法
import torch
from torch.profiler import profile, ProfilerActivity
with profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
record_shapes=True,
profile_memory=True,
with_stack=True
) as prof:
# 训练循环
for batch in dataloader:
output = model(batch.input)
loss = criterion(output, batch.target)
loss.backward()
optimizer.step()
### 打印耗时最多的 10 个操作
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))输出解读
┌─────────────────────────────────────────────────────────────┐
│ PyTorch Profiler 输出示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Name CUDA Time % │
│ ──────────────────────────────────────────────────────────│
│ cudaLaunchKernel 1000 ms 10%│
│ amp::fastMixedPrecisionKernel 2500 ms 25%│
│ layernorm_forward 1200 ms 12%│
│ embedding_forward 800 ms 8% │
│ tensor_expr_gpu_kernel 600 ms 6% │
│ aten::matmul 400 ms 4% │
│ ... │
│ │
│ 观察:amp::fastMixedPrecisionKernel 耗时最长(25%) │
│ → 可能是 Tensor Core 利用率低 │
│ │
└─────────────────────────────────────────────────────────────┘导出和可视化
### 导出 Chrome Trace 格式
prof.export_chrome_trace("trace.json")
### 在 Chrome 浏览器打开:chrome://tracing
### 可以看到每个操作的时间线和调用栈
### 导出内存快照
prof.export_memory_timeline("memory.html", device_index=0)
### 查看显存使用随时间的变化第5节:实用调试流程
┌─────────────────────────────────────────────────────────────┐
│ 性能调试完整流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 系统级:nsys timeline │
│ ├─ CPU-GPU 传输是否重叠? │
│ ├─ GPU 是否空闲等待? │
│ └─ kernel 是否并行执行? │
│ │
│ 2. Kernel 级:ncu profile │
│ ├─ SM Utilization 高还是低? │
│ ├─ 内存带宽利用率? │
│ └─ Warp 效率? │
│ │
│ 3. 热点分析: │
│ ├─ 哪个 kernel 占时间最多? │
│ ├─ 哪个操作显存占用最大? │
│ └─ 哪层耗时最长? │
│ │
│ 4. 针对性优化: │
│ ├─ 内存带宽瓶颈 → 合并访问、共享内存复用 │
│ ├─ Warp 效率低 → 减少分支分化 │
│ ├─ CPU-GPU 不同步 → Double Buffering │
│ └─ Occupancy 低 → 调整 block 大小 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ nsys/ncu 的具体命令行参数
✅ PyTorch Profiler 的具体配置选项
✅ Chrome Trace 格式的解读方法
必须理解:
🔴 nsys = 系统级 timeline,定位 CPU-GPU 协同问题
🔴 ncu = kernel 级分析,定位单 kernel 性能瓶颈
🔴 SM Utilization 低 = warp 调度不足或等待内存
🔴 Memory SOL 低 = 内存带宽瓶颈
🔴 PyTorch Profiler = 训练性能分析,定位哪层最慢学习状态:🟡 开始学习
完整迁入:原并行性能工程全文
性能工程:测量、Roofline、瓶颈定位与优化闭环 / Performance Engineering with Measurement, Roofline, and Bottleneck Analysis
📅 创建时间:2026-07-20 🏷️ 标签:#性能分析 #Roofline #Profiling #Benchmark 📚 前置知识:[[09-distributed-parallelism]] 📚 相关知识:[[/02-systems-and-performance/02-computer-architecture-and-hardware/11-performance-analysis-tools]] [[/02-systems-and-performance/02-computer-architecture-and-hardware/06-cuda-optimization]]
1. 优化的第一原则
不要优化你认为最慢的地方,要优化测量证明最慢且值得优化的地方。
完整闭环:
建立基线 → 定位热点 → 提出假设 → 单点修改 → 重新测量 → 检查正确性一次只改变关键变量,否则无法确定收益来源。
2. 先定义目标
性能可能指不同指标:
- 单次请求延迟
- 稳态吞吐
- P95/P99 尾延迟
- 每瓦性能
- 单位成本吞吐
- 最大可处理规模
- 强扩展或弱扩展效率
优化平均吞吐可能恶化尾延迟,减少时间也可能增加内存占用。目标必须与应用需求一致。
3. 建立可信基线
基准测试至少要控制:
- 输入数据与问题规模
- 编译优化等级
- 预热和首次初始化
- CPU 频率、线程绑定和后台负载
- GPU 型号、驱动和功耗状态
- 多次运行的波动
- 是否包含 I/O 和数据传输
报告结果时同时记录硬件、软件版本和测试命令。
4. Roofline 模型
Roofline 用算术强度判断程序受计算还是带宽限制:
可达到性能 ≤ min(计算峰值, 内存带宽 × 算术强度)性能
│ ───────── 计算峰值
│ __/
│ __/
│ __/ 带宽限制区
└──────────────────────── 算术强度受带宽限制
优化方向:减少数据移动、改善局部性、合并访问、提高数据复用。
受计算限制
优化方向:使用 SIMD/Tensor Core、减少无效运算、提高指令吞吐、选择合适精度。
5. CPU 分析指标
常见关注点:
- CPU 利用率和每核负载
- IPC:每周期执行指令数
- 分支预测失败
- L1/L2/L3 Cache Miss
- 内存带宽
- 上下文切换与锁等待
- NUMA 远程访问
常用工具包括 perf、VTune、Linux time、火焰图和编译器向量化报告。
6. GPU 分析指标
常见关注点:
- Kernel 执行时间
- SM 活跃度和 Occupancy
- Warp Stall 原因
- Global Memory 吞吐
- 合并访存效率
- 分支发散
- Tensor Core 利用率
- Host-Device 拷贝与同步
工具包括 Nsight Systems 和 Nsight Compute:前者观察端到端时间线,后者深入单个 Kernel 指标。
7. 集群分析指标
- 各 Rank 计算时间分布
- 集合通信耗时
- 网络吞吐和拥塞
- GPU 间拓扑路径
- Barrier 等待时间
- 数据加载和 Checkpoint 时间
- 慢节点与性能抖动
只看平均值可能掩盖 Straggler,应关注最慢 Rank 和长尾分布。
8. 常见优化误区
- 只看 CPU/GPU 利用率百分比
- 只优化微型 Kernel,忽略端到端流程
- 追求最高 Occupancy
- 用更多线程掩盖负载不均
- 没有验证结果数值正确性
- 在 Debug 构建上比较性能
- 忽略首次 JIT、缓存和预热
- 用峰值 FLOPS 直接估算真实程序
9. 优化优先级
通常按以下顺序更稳妥:
- 修正算法复杂度和不必要工作。
- 减少数据规模、拷贝和格式转换。
- 改善数据布局与局部性。
- 提高任务粒度和负载均衡。
- 并行化主要热点。
- 重叠计算与通信。
- 最后进行指令级和 Kernel 微调。
算法从 O(n²) 降到 O(n log n),通常比手写汇编更有价值。
性能实验记录模板
目标:
硬件与软件环境:
输入规模:
基线结果:
热点证据:
优化假设:
修改内容:
优化后结果:
正确性检查:
结论与下一步:下一篇:[[11-application-cases]] <!-- migrated-deep-dive:end -->
面试速答
什么是 Roofline? 它用峰值计算吞吐和“带宽 × 算术强度”共同给出性能上界,帮助判断计算受限还是内存受限。
为什么加速比不会随核心数线性增长? 串行部分、同步、调度、共享带宽、通信和负载不均都会增长或限制收益。
Profiler 与 benchmark 区别? Benchmark 稳定测量结果;Profiler 解释资源和时间分布。前者回答多快,后者帮助回答为什么。
自测
- 单个函数占 80% 时间,优化两倍后的理论整体加速约多少?
- 带宽受限代码增加算术单元为何帮助有限?
- 为什么只报告最快一次运行不可靠?
答案:1/(0.2+0.8/2)=1.67 倍;数据供给仍是瓶颈;它容易选择偶然值并掩盖波动与预热效应。