CUPY:矩阵向量乘法比向量向量乘法和小尺寸的 l2norm 更快
CUPY: matrix-vector multiplication is faster than vector-vector multiplication and l2norm for small sizes
我正在将我的 CPU 代码传输到 GPU。
在优化它的过程中,我发现了一个有争议的性能行为:
考虑计算向量的 L2 范数的简单任务。对于具有大量元素的向量,我的性能按预期缩放,但是对于少量元素 (256),它不是:
import cupy as cp
a=cp.random.rand(256)
%timeit cp.linalg.norm(a)
32.3 µs ± 159 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
现在,让我们将它与矩阵向量点积进行比较:
b=cp.random.rand(256,256)
%timeit cp.dot(a,b)
8.36 µs ± 80.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
你可以看到矩阵-向量乘积出人意料地快了 4 倍。为什么会这样?
我开始研究这个玩具问题。首先,我创建了我的自定义缩减内核:
l2norm = cp.ReductionKernel('T x','T y', 'x * x','a + b', 'y = sqrt(a)', '0', 'l2norm')
使用这个内核,我的执行时间约为 17 微秒,比 linalg.norm 快两倍,但仍然比矩阵向量点积差两倍。我确信这个内核优化得很好,所以 C++ Thurst 实现不会做得更好。
我也试过用 cp.sqrt(a.dot(a))
计算范数。我发现这是非常低效的,因为向量-向量点积 a.dot(a)
比矩阵-向量积 a.dot(b)
花费更长的时间!!!
我确实明白,对于这个小问题,性能是带宽有限的,所以大部分时间可以花在创建数组、copying/fetching 数据上,而不是算术上。但即使在这种情况下,我也希望 L2 范数比矩阵向量乘积快一点,因为它只需要 O(N) 次操作和提取,并且结果是一个数字。在矩阵向量乘积的情况下,我什至没有预先分配结果,我做了 N^2 次操作并从内存中获取 O(N^2) 个数。
对于大量元素(>1000 个元素),性能按预期扩展。
Ubuntu 18.05,anaconda 分布,python 3.8.3,cupy 8.2.,nvcc 11.0
首先,您只测量 CPU 时间,内核是异步执行的,您的测量只包括准备内核启动的部分时间,但您没有等待实际的内核执行。
如果我们更改代码以通过使用 cupyx.time.repeat
进行测量来考虑这一点,我们会得到
import cupy as cp
import cupyx
a = cp.random.rand(256)
cp.linalg.norm(a)
print(cupyx.time.repeat(cp.linalg.norm, (a,)))
b = cp.random.rand(256, 256)
print(cupyx.time.repeat(cp.dot, (a, b)))
c = cp.zeros(())
l2norm = cp.ReductionKernel(
"T x", "T y", "x * x", "a + b", "y = sqrt(a)", "0", "l2norm"
)
print(cupyx.time.repeat(l2norm, (a, c)))
结果是
norm : CPU: 32.077 us +/- 2.206 (min: 30.961 / max: 64.160) us GPU-0: 36.275 us +/- 2.223 (min: 34.880 / max: 68.512) us
dot : CPU: 9.572 us +/- 0.261 (min: 9.235 / max: 15.934) us GPU-0: 13.640 us +/- 0.347 (min: 12.896 / max: 21.440) us
l2norm : CPU: 10.216 us +/- 0.578 (min: 9.847 / max: 23.790) us GPU-0: 14.396 us +/- 0.591 (min: 13.504 / max: 27.936) us
cupy.linalg.norm 正在启动几个内核来计算范数,因此高 CPU 时间为 32 us,累积的 GPU 时间为 36 us。这里的数组大小非常小,这主要是添加几个内核的恒定开销。
dot 仅调用 cublas 函数,因此 cpu 时间大大减少,GPU 时间相当快,但随着大小的减少,这纯粹是开销。
由于生成实际内核所需的步骤,最终你的还原内核有更多的 cpu 时间,但 gpu 执行与点积大致相同。
如果我们将数组大小增加到 4096,结果如下:
norm : CPU: 31.637 us +/- 2.200 (min: 30.487 / max: 62.955) us GPU-0: 35.741 us +/- 2.215 (min: 34.336 / max: 67.008) us
dot : CPU: 9.547 us +/- 3.753 (min: 9.051 / max: 370.309) us GPU-0: 244.535 us +/- 3.791 (min: 241.952 / max: 598.624) us
l2norm : CPU: 10.170 us +/- 0.542 (min: 9.845 / max: 17.006) us GPU-0: 16.106 us +/- 0.725 (min: 15.168 / max: 29.600) us
请注意,GPU 执行时间仅针对点积发生变化,这与您的观察结果一致:)。对于其他内核,与初始开销相比,实际内核执行时间的大小仍然太小。
我正在将我的 CPU 代码传输到 GPU。 在优化它的过程中,我发现了一个有争议的性能行为:
考虑计算向量的 L2 范数的简单任务。对于具有大量元素的向量,我的性能按预期缩放,但是对于少量元素 (256),它不是:
import cupy as cp
a=cp.random.rand(256)
%timeit cp.linalg.norm(a)
32.3 µs ± 159 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
现在,让我们将它与矩阵向量点积进行比较:
b=cp.random.rand(256,256)
%timeit cp.dot(a,b)
8.36 µs ± 80.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
你可以看到矩阵-向量乘积出人意料地快了 4 倍。为什么会这样?
我开始研究这个玩具问题。首先,我创建了我的自定义缩减内核:
l2norm = cp.ReductionKernel('T x','T y', 'x * x','a + b', 'y = sqrt(a)', '0', 'l2norm')
使用这个内核,我的执行时间约为 17 微秒,比 linalg.norm 快两倍,但仍然比矩阵向量点积差两倍。我确信这个内核优化得很好,所以 C++ Thurst 实现不会做得更好。
我也试过用 cp.sqrt(a.dot(a))
计算范数。我发现这是非常低效的,因为向量-向量点积 a.dot(a)
比矩阵-向量积 a.dot(b)
花费更长的时间!!!
我确实明白,对于这个小问题,性能是带宽有限的,所以大部分时间可以花在创建数组、copying/fetching 数据上,而不是算术上。但即使在这种情况下,我也希望 L2 范数比矩阵向量乘积快一点,因为它只需要 O(N) 次操作和提取,并且结果是一个数字。在矩阵向量乘积的情况下,我什至没有预先分配结果,我做了 N^2 次操作并从内存中获取 O(N^2) 个数。
对于大量元素(>1000 个元素),性能按预期扩展。
Ubuntu 18.05,anaconda 分布,python 3.8.3,cupy 8.2.,nvcc 11.0
首先,您只测量 CPU 时间,内核是异步执行的,您的测量只包括准备内核启动的部分时间,但您没有等待实际的内核执行。
如果我们更改代码以通过使用 cupyx.time.repeat
进行测量来考虑这一点,我们会得到
import cupy as cp
import cupyx
a = cp.random.rand(256)
cp.linalg.norm(a)
print(cupyx.time.repeat(cp.linalg.norm, (a,)))
b = cp.random.rand(256, 256)
print(cupyx.time.repeat(cp.dot, (a, b)))
c = cp.zeros(())
l2norm = cp.ReductionKernel(
"T x", "T y", "x * x", "a + b", "y = sqrt(a)", "0", "l2norm"
)
print(cupyx.time.repeat(l2norm, (a, c)))
结果是
norm : CPU: 32.077 us +/- 2.206 (min: 30.961 / max: 64.160) us GPU-0: 36.275 us +/- 2.223 (min: 34.880 / max: 68.512) us
dot : CPU: 9.572 us +/- 0.261 (min: 9.235 / max: 15.934) us GPU-0: 13.640 us +/- 0.347 (min: 12.896 / max: 21.440) us
l2norm : CPU: 10.216 us +/- 0.578 (min: 9.847 / max: 23.790) us GPU-0: 14.396 us +/- 0.591 (min: 13.504 / max: 27.936) us
cupy.linalg.norm 正在启动几个内核来计算范数,因此高 CPU 时间为 32 us,累积的 GPU 时间为 36 us。这里的数组大小非常小,这主要是添加几个内核的恒定开销。
dot 仅调用 cublas 函数,因此 cpu 时间大大减少,GPU 时间相当快,但随着大小的减少,这纯粹是开销。
由于生成实际内核所需的步骤,最终你的还原内核有更多的 cpu 时间,但 gpu 执行与点积大致相同。
如果我们将数组大小增加到 4096,结果如下:
norm : CPU: 31.637 us +/- 2.200 (min: 30.487 / max: 62.955) us GPU-0: 35.741 us +/- 2.215 (min: 34.336 / max: 67.008) us
dot : CPU: 9.547 us +/- 3.753 (min: 9.051 / max: 370.309) us GPU-0: 244.535 us +/- 3.791 (min: 241.952 / max: 598.624) us
l2norm : CPU: 10.170 us +/- 0.542 (min: 9.845 / max: 17.006) us GPU-0: 16.106 us +/- 0.725 (min: 15.168 / max: 29.600) us
请注意,GPU 执行时间仅针对点积发生变化,这与您的观察结果一致:)。对于其他内核,与初始开销相比,实际内核执行时间的大小仍然太小。