【发布时间】:2020-01-16 08:17:41
【问题描述】:
我有一个简单的例子:
from numba import cuda
import numpy as np
import math
@cuda.jit
def func(i, y, z):
start = cuda.grid(1)
stride = cuda.gridsize(1)
for j in range(start, y.shape[0], stride):
# Note that these aren't my real functions but they demo the point
if i < j:
y[j, 0] = i
z[j, 0] = i + j
if i == j:
y[j, 1] = i
z[j, 1] = i * j
if i > j:
y[j, 2] = i
z[j, 2] = j
if __name__ == '__main__':
n = 30
y = np.ones((n, 3))
z = np.ones((n, 3)) * -1
device_y = cuda.to_device(y)
device_z = cuda.to_device(z)
max_i = 5
threads_per_block = 10
blocks_per_grid = math.ceil(y.shape[0]/threads_per_block[1])
for i in range(max_i):
func[blocks_per_grid, threads_per_block](i, device_y, device_z)
out = device_y.copy_to_host()
print(out)
输出应该是这样的:
[[1. 0. 4.]
[0. 1. 4.]
[1. 2. 4.]
[2. 3. 4.]
[3. 4. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]]
但是,当max_i 很大时,大部分时间都花在调用 CUDA 内核上,我想让这个内核尽可能快。所以,我试图弄清楚如何将max_i for 循环移动到内核中,但看起来我正在进入竞争条件。这是我目前拥有的:
from numba import cuda
import numpy as np
import math
@cuda.jit
def func(max_i, y, z):
a, b = cuda.grid(2)
a_stride, b_stride = cuda.gridsize(2)
for i in range(a, max_i, a_stride):
for j in range(b, y.shape[0], b_stride):
if i < j:
y[j, 0] = i
z[j, 0] = i + j
if i == j:
y[j, 1] = i
z[j, 1] = i * j
if i > j:
y[j, 2] = i
z[j, 2] = j
if __name__ == '__main__':
n = 30
y = np.ones((n, 3))
z = np.ones((n, 3)) * -1
device_y = cuda.to_device(y)
device_z = cuda.to_device(z)
max_i = 5
threads_per_block = (1, 10)
blocks_per_grid = (max_i, math.ceil(y.shape[0]/threads_per_block[1]))
func[blocks_per_grid, threads_per_block](max_i, device_y, device_z)
out = device_y.copy_to_host()
print(out)
这个(不正确的)输出看起来像:
[[1. 0. 4.]
[0. 1. 4.]
[1. 2. 4.]
[1. 3. 4.] # Should be [2. 3. 4.]
[3. 4. 1.]
[4. 1. 1.]
[3. 1. 1.] # Should be [4. 1. 1.]
[3. 1. 1.] # Should be [4. 1. 1.]
[3. 1. 1.] # Should be [4. 1. 1.]
[3. 1. 1.] # Should be [4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.] # Should be [4. 1. 1.]
[0. 1. 1.]] # Should be [4. 1. 1.]
如上所述,我如何通过使用单个内核获得正确答案,同时使该内核尽可能快(即避免原子操作)?
【问题讨论】:
-
从您的第一个示例到第二个示例,大概您正在演示将 5 个
max_i循环带入内核,您进行了两项更改:1.您将网格增加了 5 倍,2.您在内核中添加了一个循环。从概念上讲,您只需要其中一项更改。但这里更大的问题是,将该循环排除在内核之外意味着您在每次内核调用时都有一个网格范围的同步,我认为这对于您的算法是必要的。内核内部的循环并没有消除对它的需要。原子不会解决这个问题。你需要一个内核网格同步,而 numba cuda 没有这个 -
是的,你的观察是正确的。我添加了第二个维度,我认为只有在所有
i=1启动(或同时)之后才能强制执行i=2。然后,我会让i=1结果(以及所有i=odd)写入单独的y_odd和z_odd,而i=2(以及所有i=even)将写入y_even和z_even.但是我意识到,如果线程总数大于2 * x.shape[0],这将是一个问题,因为i=3将有足够的线程与i=1同时执行,现在我有相同的竞争条件,但对于赔率和偶数。我有什么选择? -
在我看来,明智的选择是您在上一个问题中的代码实现。我没有费心解决这个问题,因为您没有提供测试用例,不,我不会尝试对内核进行逆向工程,以找出如何用足够的代码包装它来制作测试用例。您可能已经拥有它并决定不提供它。所以我决定不关注。没有测试用例 IMO 的性能问题“不清楚且无用”。
-
如果您真的非常想将网格同步推送到内核中,那么正确的方法在我所知道的任何 cuda python 实现中都没有公开(还)。您可以将内核构建到 CUDA C++ 中的库中,并使用 python ctypes 从 python 调用它。可能还有其他方法。但如果不了解问题的范围和可能的一些分析,我不会做所有这些工作。仅使用内核无法做到这一点。
-
因此,考虑到在内核外部使用
ifor 循环会产生正确的答案,但会导致许多缓慢/昂贵的内核调用,是否有更好的方法从Pythonifor 循环?尝试将 for 循环推入内核的目的是为了加快速度,我觉得自己离我们如此之近又如此遥远。