本文只讨论通用性能分析方法,不披露任何未结束项目(依旧是博主的先导杯还没结束)的模型(先导杯赛题pangu-weather)结构、热点分布、优化方案或评测结果。

摘要

DCU 性能优化最常见的误区,是拿一个时间或一个显存数字直接下结论。端到端 wall time、设备事件、Profiler 算子时间、PyTorch allocated、PyTorch reserved 和整卡 VRAM 回答的是不同问题。只有把这些口径分开,优化结果才可解释、可复现。

本文给出一套适用于 PyTorch/DCU 推理和训练任务的性能分析流程:如何建立基线、如何放置同步、如何导出 trace、如何判断算子热点、如何测量显存,以及怎样避免首次编译和缓存状态污染实验。

关键词:DCU、PyTorch Profiler、性能分析、显存测量、Benchmark

1. 先定义要测什么

一次完整任务至少有四种时间:

时间口径 包含内容 适合回答的问题
进程wall time 启动、导入、编译、加载、数据、计算、保存 用户实际等待多久
样本端到端时间 规定区域内的搬运、计算和同步 正式业务性能
设备事件时间 两个设备event之间的GPU/DCU工作 某段设备工作多久
Profiler算子时间 按operator/kernel聚合 时间花在哪些算子

如果目标是优化正式推理,最重要的是样本端到端时间。微基准和 Profiler 用来解释它,而不是替代它。

2. 为什么同步决定了时间是否可信

设备计算通常异步提交。下面的写法只测到了提交开销:

start = time.perf_counter()
y = model(x)
elapsed = time.perf_counter() - start

更可靠的主机计时:

torch.cuda.synchronize()
start = time.perf_counter()

y = model(x)

torch.cuda.synchronize()
elapsed = time.perf_counter() - start

HIP版PyTorch通常继续使用 torch.cuda.synchronize() API。多 stream 程序还要确认被测工作是否全部在当前同步范围内;只同步默认 stream 可能遗漏 copy stream。

正式评测如果规定了时间边界,应以规定为准。不能为了得到更小数字,把必要搬运、同步或后处理移到计时区外。

3. 冷启动与稳定段要分开记录

首轮可能包含:

  • Python模块首次导入;
  • 自定义扩展编译和加载;
  • 动态库初始化;
  • 后端算法选择;
  • 内存池扩张;
  • 文件缓存和数据页加载。

建议同时保存原始序列和稳定段统计:

times = []
for sample in samples:
    torch.cuda.synchronize()
    begin = time.perf_counter()
    run(sample)
    torch.cuda.synchronize()
    times.append(time.perf_counter() - begin)

print("all:", times)
print("steady_mean:", sum(times[2:]) / len(times[2:]))

这里跳过前两个样本只是分析示例,不应擅自改变正式规则。原始时间必须完整保留。

4. 用 PyTorch Profiler 找方向

一个通用的推理采样模板:

import torch

activities = [torch.profiler.ProfilerActivity.CPU]
if torch.cuda.is_available():
    activities.append(torch.profiler.ProfilerActivity.CUDA)

with torch.profiler.profile(
    activities=activities,
    record_shapes=True,
    profile_memory=True,
    with_stack=False,
) as prof:
    with torch.inference_mode():
        output = model(inputs)
    torch.cuda.synchronize()

print(prof.key_averages().table(
    sort_by="self_cuda_time_total",
    row_limit=30,
))
prof.export_chrome_trace("trace.json")

在 HIP 版 PyTorch 中,Profiler 的字段可能仍沿用 CUDA 命名。判断依据应是当前PyTorch是否为HIP构建,而不是字段名字。

5. 如何读算子表

算子表至少关注五列:

self device time
total device time
调用次数
平均单次时间
输入shape

不同模式代表不同问题:

  • 总时间高、调用少:适合优化单个大算子;
  • 总时间高、调用多、单次很短:可能是调度和中间张量问题;
  • CPU时间高、设备时间低:可能是Python循环或同步;
  • device memory增长明显:需要检查工作区和生命周期;
  • shape种类很多:专用kernel可能难以覆盖。

不要只按总时间排序就开始写kernel。先确认该算子是否在端到端关键路径、是否能被后端自动融合、输出是否被复用,以及替换它会不会增加显存。

6. Chrome Trace 看调用关系

聚合表告诉你“谁最贵”,trace告诉你“为什么会贵”。打开 trace.json 后重点观察:

  • kernel之间是否有大段空洞;
  • 是否出现频繁H2D/D2H;
  • CPU线程是否及时提交工作;
  • 相邻逐元素算子是否反复读写同一大张量;
  • stream之间是否真正重叠;
  • 是否有意外同步把流水切断。

一次性能问题往往不是某个kernel慢,而是十几个短kernel之间存在同步或临时分配。

7. PyTorch显存的四个数字

常用接口:

torch.cuda.memory_allocated()
torch.cuda.memory_reserved()
torch.cuda.max_memory_allocated()
torch.cuda.max_memory_reserved()

含义:

  • allocated:仍被活动tensor占用;
  • reserved:PyTorch allocator向运行时申请并保留;
  • max_allocated:本轮活动tensor峰值;
  • max_reserved:本轮内存池峰值。

测试单轮峰值前可以重置统计:

torch.cuda.reset_peak_memory_stats()
run_once()
torch.cuda.synchronize()
print(torch.cuda.max_memory_allocated())
print(torch.cuda.max_memory_reserved())

reserved 大于 allocated 并不意味着泄漏。它可能是缓存分配器为了复用而保留的block。真正的泄漏通常表现为多轮运行后活动引用和峰值持续增长。

8. 为什么整卡显存与PyTorch不同

设备管理工具看到的整卡显存通常还包含:

HIP context
已加载代码模块
数学库workspace
通信库buffer
PyTorch allocator
其他进程

因此应同时记录两套数据:

框架内:allocated / reserved
框架外:设备管理工具的进程或整卡VRAM

如果两者变化方向相反,先检查采样时机、扩展加载、首次库初始化和同卡其他进程,不要马上得出“代码泄漏”的结论。

9. empty_cache()不是通用优化

torch.cuda.empty_cache() 只能归还没有活动引用的缓存块。它不能释放仍被tensor使用的显存,而且可能导致下一轮重新申请。

只有在明确需要把空闲缓存归还给其他进程,或正在诊断allocator行为时,才应考虑调用。性能循环中每轮调用通常会增加同步和分配开销。

降低峰值的优先顺序应该是:

  1. 找出峰值时同时存活的tensor;
  2. 缩短临时tensor生命周期;
  3. 复用固定buffer;
  4. 调整算法工作区;
  5. 最后再讨论allocator缓存策略。

10. 一个可复现的实验矩阵

每个候选只改一个变量:

字段 示例内容
variant baseline / candidate_a
device 设备型号与架构
software DTK、PyTorch、Python版本
input 固定shape和dtype
repetitions 原始样本数或重复次数
timing 完整时间数组、稳定段均值
memory max allocated、max reserved、整卡峰值
correctness 最大误差、平均误差或任务指标

不要只保存“最快的一次”。建议至少重复多轮,报告中位数或稳定均值,并保留原始数据。

11. 性能优化的三道门

一个候选进入最终版本前应同时通过:

正确性门:输出误差在允许范围内
性能门:端到端收益稳定,不只是微基准更快
资源门:显存、编译时间和包体积没有不可接受回退

只通过微基准的实现仍然是实验原型。真正可提交或可上线的优化必须在完整工作负载中成立。

参考与依赖说明

本文示例基于 PyTorch Profiler、PyTorch accelerator memory API 和通用HIP/DCU设备管理方法。实际字段和命令应以当前DTK、PyTorch及设备官方文档为准。文章不包含任何项目专有性能数据或优化实现。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐