我有n
套,有限宇宙的子集。我想计算n*n
矩阵,其中(I, J)
条目包含集合I
和集合J
的交集的基数。 n
大约为50000
。
我的想法是将矩阵拆分成足够小的块,以便每个条目有一个线程。每个线程都应使用bitwise and
计算交集。
是否有更有效的方法来解决这个问题?
答案 0 :(得分:1)
我假设您要按照您的描述计算它:实际使用按位和位集计算每对集合的交集。
使用正确的数学设置,你实际上是计算两个向量的外积,所以我会考虑高性能线性代数。
性能的关键是减少内存流量,这意味着尽可能将内容保存在寄存器中。绝大多数重要因素是你的元素巨大;它需要6250个32位字才能存储一套!例如,cuda计算能力3.0的整个多处理器在寄存器中只能容纳10组。
您可能想要做的是将每个元素分散到整个线程块中。块中有896个线程,每个块有7个寄存器,可以存储一组200704元素。使用cuda计算能力3.0,每个块将有36个寄存器。
最简单的实现是让每个块拥有输出矩阵的一行。它加载第二个向量的相应元素并将其存储在寄存器中,然后迭代第一个向量的所有元素,计算交集,计算和减少popcount,然后将结果存储在输出向量中。
此优化应将内存读取的总数减少2倍,因此可能会使性能提高一倍。
最好是让每个块同时拥有3-4行输出矩阵,并将第二个向量的相应3-4个元素加载到寄存器中。然后该块迭代第一个寄存器的所有元素,并且每个元素计算它可以的3-4个交叉点,将结果存储在输出矩阵中。
此优化可将内存流量减少3-4倍。
答案 1 :(得分:1)
完全不同的方法是单独使用Universe的每个元素:对于Universe的每个元素,您计算实际包含该元素的哪些集合,然后(原子地)递增输出矩阵的相应条目。 / p>
渐近地,这应该比计算集合的交叉点更有效。不幸的是,这听起来很难有效。
改进就是一次使用宇宙中的4个元素。您将所有设置拆分为16个桶,具体取决于该组包含的4个元素中的哪一个。然后,对于16 * 16个可能的桶对中的每一个,您遍历桶中的所有矢量对,并(原子地)适当地更新矩阵的相应条目。
这应该比上述版本更快,但仍可能难以实现。
为了减少完成所有同步的难度,您可以将所有输入集分成k
个n/k
组。然后,(i,j)
- 线程(或warp或块)仅对输出矩阵的相应块进行更新。
答案 2 :(得分:1)
解决问题的另一种方法是将 Universe 分成每个1024个元素的较小分区,并计算宇宙这一部分中交叉点的大小。
我不确定我是否描述得那么好;基本上你是在计算
A[i,j] = sum((k in v[i]) * (k in w[j]) for k in the_universe)
其中v
和w
是两个集合列表,k in S
如果为1
则为0
,否则为k
。关键是要对索引进行置换,以便k
位于外部循环而不是内部循环中,但为了提高效率,您必须连续使用多个{{1}}一次,而不是一次一个。
也就是说,您将输出矩阵初始化为全零,并且对于每个1024个Universe元素的块,您可以计算交叉点的大小并将结果累积到输出矩阵中。
我选择了1024,因为我想你会有一个数据布局,这可能是你从设备内存读取时仍然可以获得全部内存带宽的最小尺寸,并且warp中的所有线程一起工作。 (如果你比我更了解,或者你没有使用nVidia以及你正在使用的任何其他GPU可以更好地工作),请适当调整它。
现在您的元素大小合理,现在可以吸引传统的线性代数优化来计算此产品。我可能会做以下事情:
为每个warp分配了大量的输出矩阵行。它从第二个向量中读取相应的元素,然后遍历第一个向量,计算产品。
您可以让所有warp独立运行,但最好还是执行以下操作:
您可以将加载的元素存储在共享内存中,但是您可能会在寄存器中获得更好的结果。每个扭曲只能计算与其所保持的设定元素的交点,但是在这样做之后,经线可以全部旋转哪些经线保持哪些元素。
如果你沿着这些方向进行了足够的优化,你可能会达到不再受内存限制的程度,这意味着你可能不必进行最复杂的优化(例如描述的共享内存方法)上面可能已经足够了。)