性能最佳实践#

此处我们收集了一些用于提升 CuPy 性能的技巧和建议。

基准测试#

在尝试优化代码之前,首先找出性能瓶颈至关重要。为了帮助建立基准测试基线,CuPy 提供了一个有用的工具 cupyx.profiler.benchmark(),用于测量 Python 函数在 CPU 和 GPU 上的执行时间。

>>> from cupyx.profiler import benchmark
>>>
>>> def my_func(a):
...     return cp.sqrt(cp.sum(a**2, axis=-1))
...
>>> a = cp.random.random((256, 1024))
>>> print(benchmark(my_func, (a,), n_repeat=20))  
my_func             :    CPU:   44.407 us   +/- 2.428 (min:   42.516 / max:   53.098) us     GPU-0:  181.565 us   +/- 1.853 (min:  180.288 / max:  188.608) us

由于 GPU 执行与 CPU 执行是异步运行的,GPU 编程中的一个常见陷阱是错误地使用 CPU 计时工具(例如 Python 标准库中的 time.perf_counter() 或 IPython 的 %timeit magic 命令)来测量经过的时间,这些工具对 GPU 运行时一无所知。cupyx.profiler.benchmark() 通过在被测量函数之前和之后,在当前流上设置 CUDA 事件,并在结束事件上进行同步来解决此问题(详情请参阅流和事件)。下面我们概述 cupyx.profiler.benchmark() 内部的工作原理。

>>> import time
>>> start_gpu = cp.cuda.Event()
>>> end_gpu = cp.cuda.Event()
>>>
>>> start_gpu.record()
>>> start_cpu = time.perf_counter()
>>> out = my_func(a)
>>> end_cpu = time.perf_counter()
>>> end_gpu.record()
>>> end_gpu.synchronize()
>>> t_gpu = cp.cuda.get_elapsed_time(start_gpu, end_gpu)
>>> t_cpu = end_cpu - start_cpu

此外,cupyx.profiler.benchmark() 会运行几次热身,以减少计时波动并排除首次调用中的开销。

一次性开销#

在对 CuPy 代码进行基准测试时,请注意这些开销。

上下文初始化#

在进程中首次调用 CuPy 函数时,可能需要几秒钟。这是因为 CUDA 驱动程序在 CUDA 应用程序中的第一次 CUDA API 调用期间会创建一个 CUDA 上下文。

核函数编译#

CuPy 使用即时 (on-the-fly) 核函数合成。当需要调用核函数时,它会编译针对给定参数的维度和数据类型优化的核函数代码,将其发送到 GPU 设备,然后执行该核函数。

CuPy 在进程内部缓存发送到 GPU 设备的核函数代码,这减少了后续调用时的核函数编译时间。

编译后的代码也缓存在目录 ${HOME}/.cupy/kernel_cache 中(可以通过设置 CUPY_CACHE_DIR 环境变量来覆盖该路径)。这允许在不同进程之间重用已编译的核函数二进制文件。

深度性能分析#

正在建设中。要使用 NVTX/rocTX 范围进行标记,可以使用 cupyx.profiler.time_range() API。要启动/停止性能分析器,可以使用 cupyx.profiler.profile() API。

使用 CUB/cuTENSOR 后端进行归约及其他例程#

对于归约操作(例如 sum()prod()amin()amax()argmin()argmax())以及基于它们构建的许多其他例程,CuPy 提供了自己的实现,以便开箱即用。然而,有一些专门致力于进一步加速这些例程的工作,例如 CUBcuTENSOR

为了在适用情况下支持性能更优的后端,从 v8 版本开始,CuPy 引入了一个环境变量 CUPY_ACCELERATORS,允许用户指定所需的后端(以及尝试它们的顺序)。例如,考虑对一个 256 立方数组求和

>>> from cupyx.profiler import benchmark
>>> a = cp.random.random((256, 256, 256), dtype=cp.float32)
>>> print(benchmark(a.sum, (), n_repeat=100))  
sum                 :    CPU:   12.101 us   +/- 0.694 (min:   11.081 / max:   17.649) us     GPU-0:10174.898 us   +/-180.551 (min:10084.576 / max:10595.936) us

我们可以看到它运行大约需要 10 毫秒(在此 GPU 上)。然而,如果使用 CUPY_ACCELERATORS=cub python 启动 Python 会话,我们可以免费获得约 100 倍的加速(仅需约 0.1 毫秒)。

>>> print(benchmark(a.sum, (), n_repeat=100))  
sum                 :    CPU:   20.569 us   +/- 5.418 (min:   13.400 / max:   28.439) us     GPU-0:  114.740 us   +/- 4.130 (min:  108.832 / max:  122.752) us

CUB 是一个与 CuPy 一同提供的后端。它还加速其他例程,例如包含扫描(例如:cumsum())、直方图、稀疏矩阵向量乘法(在 CUDA 11 中不适用)和 ReductionKernel。cuTENSOR 为二进制逐元素 ufuncs、归约和张量收缩提供了优化的性能。如果安装了 cuTENSOR,例如设置 CUPY_ACCELERATORS=cub,cutensor,则会首先尝试 CUB,如果 CUB 不提供所需的支持,则回退到 cuTENSOR。如果两个后端都不适用,则回退到 CuPy 的默认实现。

请注意,虽然一般来说加速后的归约更快,但根据数据布局可能会有例外情况。特别是,CUB 归约仅支持沿连续轴进行归约。无论如何,我们建议进行一些基准测试,以确定 CUB/cuTENSOR 是否提供更好的性能。

注意

CuPy v11 及更高版本默认使用 CUB。要将其关闭,您需要显式指定环境变量 CUPY_ACCELERATORS=""

使用流重叠工作#

正在建设中。

使用 JIT 编译器#

正在建设中。目前请参阅JIT 核函数定义获取快速介绍。

优先使用 float32 而非 float64#

正在建设中。