python - 矢量化 itertools combination_with_replacement
问题描述
我的目标是通过使用我的 GPU 来加快创建组合列表的速度。我怎样才能做到这一点?
例如,以下代码创建了一个包含 260 个文本字符串的列表,范围从“aa”到“jz”。然后我们使用 itertools combination_with_replacement() 来创建该列表中 R 元素的所有可能组合。timeit 的使用表明,超过 3 个元素,提取这些组合的列表会呈指数级减慢。我怀疑这可以用 numba cuda 来完成,但我不知道怎么做。
import timeit
timeit.timeit('''
from itertools import combinations_with_replacement
combo_count = 2
alphabet = 'a'
alpha_list = []
item_list = []
for i in range(0,26):
alpha_list.append(alphabet)
alphabet = chr(ord(alphabet) + 1)
for first_letter in alpha_list[0:10]:
for second_letter in alpha_list:
item_list.append(first_letter+second_letter)
print("Length of item list:",len(item_list))
combos = combinations_with_replacement(item_list,combo_count)
cmb_lst = [bla for bla in combos]
print("Length of list of all {} combinations: {}".format(combo_count,len(cmb_lst)))
''', number=1)
解决方案
正如评论中提到的,没有办法直接combinations_with_replacement()
从itertools
库中“矢量化”调用(使用 Numba CUDA)。Numba CUDA不能那样工作。
但是,我相信应该可以使用 Numba CUDA 生成等效的结果数据集,在itertools
某些情况下,它的运行速度似乎比库函数快。我想可能有很多方法可以做到这一点,我没有声称我描述的方法在任何方面都是最佳的。它当然不是,当然可以让它跑得更快。itertools
然而,根据我的测试,即使是这种不太优化的方法也可以在 V100 GPU 上运行比 python 快约 10 倍的特定测试用例。
作为背景,我认为this和this(或同等材料)是必不可少的阅读材料。
综上所述,有选择、有替换的n
项目组合数的公式由下式给出:k
(n-1+k)!
-------- (Formula 1)
(n-1)!k!
在下面的代码中,我将上述计算封装在count_comb_with_repl
(设备)和host_count_comb_with_repl
(主机)函数中。事实证明,我们可以使用这个基本计算,对 和 进行级联较小的选择序列n
,k
以驱动整个计算过程来计算仅给定最终结果数组的索引的组合。为了形象化我们正在做的事情,有一个简单的例子会有所帮助。让我们以 3 个项目和 3 个选项为例。从零开始索引项目,可能性数组如下所示:
n = 3, k = 3
index choices first digit calculation
0 0,0,0 -----------------
1 0,0,1
2 0,0,2
3 0,1,1 equivalent to n = 3, k = 2
4 0,1,2
5 0,2,2 -----------------
6 1,1,1 -----------------
7 1,1,2 equivalent to n = 2, k = 2
8 1,2,2 -----------------
9 2,2,2 equivalent to n = 1, k = 2
上面列表的长度是通过将n
= 3 和k
= 3 的值代入公式 1 得出的。理解我提出的方法的关键观察是,仅给定索引来计算选择结果的第一位,我们可以计算0 和 1 之间的分界点,例如通过观察考虑到第一选择索引为 0 的结果,通过将n
= 3 和k
= 2 的值代入公式 1 来给出该范围的长度。因此,如果我们给定的索引小于这个值 (6) 那么我们知道第一个数字是 0。如果它大于这个值那么我们知道第一个数字是 1 或 2,并且通过适当的偏移我们可以重新计算下一个范围(对应于第一个数字1) 看看我们的指数是否在这个范围内。
一旦我们知道了第一个数字,我们就可以重复这个过程(使用适当的列表缩减和偏移)来找到下一个数字,下一个数字,等等。
这是实现上述方法的python代码。正如我所提到的,对于一个测试用例,n=260
它k=4
在我的 V100 上运行不到 3 秒。
$ cat t2.py
from numba import cuda,jit
import numpy as np
@cuda.jit(device=True)
def get_next_count_comb_with_repl(n,k,prev):
return int(round((prev*(n))/(n+k)))
@cuda.jit(device=True)
def count_comb_with_repl(n,k):
mymax = max(n-1,k)
ans = 1.0
cnt = 1
for i in range(mymax+1, n+k):
ans = ans*i/cnt
cnt += 1
return int(round(ans))
#intended to be identical to the previous function
#I just need a version I can call from host code
def host_count_comb_with_repl(n,k):
mymax = max(n-1,k)
ans = 1.0
cnt = 1
for i in range(mymax+1, n+k):
ans = ans*i/cnt
cnt += 1
return int(round(ans))
@cuda.jit(device=True)
def find_first_digit(n,k,i):
psum = 0
count = count_comb_with_repl(n, k-1)
if (i-psum) < count:
return 0,psum
psum += count
for j in range(1,n):
count = get_next_count_comb_with_repl(n-j,k-1,count)
if (i-psum) < count:
return j,psum
psum += count
return -1,0 # error
@cuda.jit
def kernel_count_comb_with_repl(n, k, l, r):
for i in range(cuda.grid(1), l, cuda.gridsize(1)):
new_ll = n
new_cc = k
new_i = i
new_digit = 0
for j in range(k):
digit,psum = find_first_digit(new_ll, new_cc, new_i)
new_digit += digit
new_ll -= digit
new_cc -= 1
new_i -= psum
r[i+j*l] = new_digit
combo_count = 4
ll = 260
cl = host_count_comb_with_repl(ll, combo_count)
print(cl)
# bug if cl > 2G
if cl < 2**31:
my_dtype = np.uint8
if ll > 255:
my_dtype = np.uint16
r = np.empty(cl*combo_count, dtype=my_dtype)
d_r = cuda.device_array_like(r)
block = 256
grid = (cl//block)+1
#grid = 640
kernel_count_comb_with_repl[grid,block](ll, combo_count, cl, d_r)
r = d_r.copy_to_host()
print(r.reshape(combo_count,cl))
$ time python t2.py
194831715
[[ 0 0 0 ... 258 258 259]
[ 0 0 0 ... 258 259 259]
[ 0 0 0 ... 259 259 259]
[ 0 1 2 ... 259 259 259]]
real 0m2.212s
user 0m1.110s
sys 0m1.077s
$
(上面的测试用例:n=260,k = 4,在我的系统上使用 OP 的代码需要大约 30 秒。)
这应该被认为是一个想法的草图。我没有声称它没有缺陷。这种类型的问题会迅速耗尽 GPU 上的内存(对于足够大的n
和/或的选择k
),您唯一的迹象可能是来自 numba 的粗略的内存不足错误。
是的,上面的代码不会产生aa
through的连接,jz
但这只是使用结果的索引练习。您可以根据需要使用结果索引来索引项目数组,将 0,0,0,1 之类的结果转换为aa
, aa
, aa
,之类的结果ab
这不是全面的性能胜利。他们的 python 方法对于较小的测试用例仍然更快,而较大的测试用例(例如 n = 260,k = 5)将超过 GPU 上的可用内存。
推荐阅读
- java - 如何集成 Gradle、IntelliJ 和 SpotBugs?
- scala - Scala 中的偏函数
- quarkus - 我需要向 sam.jvm.yaml 添加 CORS 参数,有没有办法在 Quarkus 中做到这一点?
- react-native - 尝试使用 xcodebuild 创建的 .app 文件运行排毒测试时出错
- django - Django——查看 MultiValueDictKeyError
- c# - C#将项目添加到对象列表字段
- mysql - 优化此 sql 查询的替代方法是什么?
- reactjs - 在用户登录到 React 路由器 dom 中的帐户后,您通常如何重定向用户?
- devexpress - 如何在 GeneXus 中集成 DevExpress
- sql - 将两个表结果组合成 JSON - SQL server