准确测试cuda代码执行性能

NOTE

参考自文章:https://blog.speechmatics.com/cuda-timings

强烈推荐大家去阅读原文

GPU 算子性能测试优化点总结

优化点 核心原理 解决的问题
主机-设备同步 (Host-Device Sync) CUDA 核函数是异步执行的。CPU 只是把任务丢进队列就继续往下跑了。 避免只测到了“分发任务”的时间,而不是“执行任务”的时间。
CUDA Events 在 GPU 指令流中插入“时间戳”标记,由 GPU 硬件直接记录。 减少 CPU 侧 perf_counter 带来的系统调用开销和内核启动(Launch)干扰。
预热 (Warmup) 排除 JIT 编译、cudnn 算子自动选择(autotune)、显存重新分配和延迟加载等一次性开销。 避免初次运行的巨大延迟拉高平均值,使数据更符合稳定运行状态。
固定时钟频率 (Fixed Clocks) 现代 GPU 会根据功耗和温度动态调频(Boost/Throttling)。 消除因硬件状态波动导致的测试结果不一致,确保实验可重复。
清除缓存 (Cache Flush) 大部分 GPU 算子会受益于 L2 缓存。如果连续测试同一算子,后续运行会因为缓存命中而显得极快。 模拟真实的、非连续命中的运行环境,反映算子的真实带宽/计算需求。
休眠/CUDA 图 (Sleep/Graphs) 对于极短的算子,内核启动时间可能超过执行时间,导致 GPU 等待 CPU 发指令。 通过 _sleep 让指令队列堆积或使用 CUDAGraphs 将多个小算子打包,消除启动间隔。

测试脚本

这个脚本集成上述所有技巧,旨在为你提供一个准确、可重复的算子性能测试框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import torch
import torch.utils.benchmark as benchmark
import numpy as np
from typing import Callable

def flush_cache():
    """清除 L2 缓存。A100 的 L2 约为 40MB,这里分配 40MB 零向量并操作它。"""
    # 这里的容量根据你的 GPU 型号调整,通常 40MB-80MB 足够
    cache_size = 40 * 1024 * 1024
    x = torch.empty(cache_size, dtype=torch.int8, device='cuda')
    x.zero_()

def benchmark_operator(op_func: Callable, warmup_steps: int = 5, test_steps: int = 10):
    """
    专业的算子性能测试函数
    """
    # 1. 预热 (Warmup)
    # 排除 JIT 编译和 cuDNN autotune 的干扰
    for _ in range(warmup_steps):
        op_func()

    torch.cuda.synchronize()

    # 2. 使用 CUDA Events 进行精确计时
    start_events = [torch.cuda.Event(enable_timing=True) for _ in range(test_steps)]
    end_events = [torch.cuda.Event(enable_timing=True) for _ in range(test_steps)]

    for i in range(test_steps):
        # 3. 清除缓存 (可选,视测试需求而定)
        flush_cache()

        # 4. 插入少量时钟周期休眠以确保 GPU 队列饱和(针对极快算子)
        torch.cuda._sleep(10_000_000)

        start_events[i].record()
        op_func()
        end_events[i].record()

    # 等待所有操作完成
    torch.cuda.synchronize()

    # 计算平均时间 (单位:毫秒)
    times = [s.elapsed_time(e) for s, e in zip(start_events, end_events)]
    times_array = np.array(times)

    avg_time = np.mean(times_array)
    median_time = np.median(times_array)
    p90_time = np.percentile(times_array, 90)
    p99_time = np.percentile(times_array, 99)

    print(f"Average Execution Time: {avg_time:.4f} ms")
    print(f"Median Time: {median_time:.4f} ms")
    print(f"P90 Time: {p90_time:.4f} ms")
    print(f"P99 Time: {p99_time:.4f} ms")
    print(f"Min Time: {min(times):.4f} ms | Max Time: {max(times):.4f} ms")
    return avg_time

# --- 使用示例 ---
if __name__ == "__main__":
    # 定义你想要测试的算子,例如一个矩阵乘法
    A = torch.randn(2048, 2048, device='cuda')
    B = torch.randn(2048, 2048, device='cuda')

    def my_op():
        return torch.matmul(A, B)

    print("Benchmarking MatMul...")
    benchmark_operator(my_op)

补充建议:

  1. 关于固定时钟:脚本中未包含 nvidia-smi 命令,因为这通常需要 sudo 权限。建议在运行 Python 脚本前,在终端手动执行:
    • 启用持久模式:sudo nvidia-smi -pm 1
    • 锁定频率(以 A100 为例):sudo nvidia-smi -lgc 1215
  2. PyTorch 官方工具:如果你追求更简便的封装,可以使用 torch.utils.benchmark.Timer,它内部已经处理了同步和预热的逻辑,但手动编写上述脚本能让你在处理“缓存刷新”和“底层休眠”时有更高的自由度。