我的项目包含了很多fill
、copy
等基本操作。然而,我是CUDA编程的新手,我目前的实现只是使用for
循环来操作device_vector
,这远不如使用迭代器有效。
我的问题是:如何使用迭代器(例如counting
/permutation_iterator
等)实现以下函数?
1.批量从指定索引中获取fill
、sequence
和transform
值。
玩具示例:
len1 = 3, len2 = 5;
vecA.size() = (len1 + 2) * len2;
vecB.size() = len2;
// {} are just to help show the difference
// fill vecA in batch (len1) from index 1 using each value of vecB, vecA` is original vector
vecB = [ 1, 5, 2, 4, 2 ]
vecA`= [1, {1, 1, 1}, 1, 1, {1, 1, 1}, 1, 1, {1, 1, 1}, 1, 1, {1, 1, 1}, 1, 1, {1, 1, 1}, 1]
vecA = [1, {1, 1, 1}, 1, 1, {5, 5, 5}, 1, 1, {2, 2, 2}, 1, 1, {4, 4, 4}, 1, 1, {2, 2, 2}, 1]
// multiply values in vecA with 2 in batch (len1) from index 1, vecC` is original vector
vecC.size() = (len1 + 2) * len2;
vecC`= [1, {1, 1, 1}, 1, 1, {2, 2, 2}, 1, 1, {3, 3, 3}, 1, 1, {4, 4, 4}, 1, 1, {5, 5, 5}, 1]
vecC = [1, {2, 2, 2}, 1, 1, {4, 4, 4}, 1, 1, {6, 6, 6}, 1, 1, {8, 8, 8}, 1, 1, {10, 10, 10}, 1]
// sequence vecD(len1 * len2) in batch (len1)
vecD = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]
字符串
下面的代码使用了一个for
循环,我猜它的效率远远低于它的效率:
size_t len1 = 3, len2 = 5;
thrust::device_vector<int> vecA((len1 +2) * len2);
thrust::device_vector<int> vecC((len1 +2) * len2);
thrust::device_vector<int> vecB(len2);
int offset_sta = 0;
int offset_end = 0;
for (size_t i = 0; i < len2; i++)
{
offset1 = i * (len1 + 2) + 1; // +1 means from specified index 1 in each batch
thrust::fill_n(vecA.begin() + offset1, len1, vecB.begin()[i]);
thrust::transform(vecC.begin() + offset1, vecC.begin() + offset1 + len1, vecC.begin() + offset1, scalar_mult_functor<int>(2));
}
// sequence
thrust::device_vector<int> vecD(len1 * len2);
for (size_t i = 0; i < len2; i++)
{
offset1 = i * len1;
offset2 = (i + 1) * len1;
thrust::sequence(vecD.begin() + offset1, vecD.begin() + offset2);
}
型
copy
子向量批量转换为另一个向量。
玩具示例:
len1 = 2, len2 = 4, len3 = 5;
// copy values of vecA(len1 * len3) in batch (len1) to vecB(len2 * len3)
vecA = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
vecB = [1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 5, 6, 7, 8, 7, 8, 9, 10, 9, 10]
型
为了实现这一点,我简单地使用两个for
循环来复制值,但显然这是低效的。
reduce_sum
批量(len1
)中的向量值(不是一个值乘以一个值)。
玩具示例:
len1 = 4, len2 = 3;
vecA.size() = len1 * len2;
vecB.size() = len1
vecA = [1, 2, 3, 4, 1, 1, 1, 1, 2, 2, 2, 2]
vecB = {1, 2, 3, 4} + {1, 1, 1, 1} + {2, 2, 2, 2} = [4, 5, 6, 7]
型
上述操作更像是在CPU上使用STL向量对2D向量执行的操作。我查看了一些教程,并试图用迭代器实现它们,但得到了错误的结果。
1.但我还有一个问题对vecB
中的批处理执行reduce sum(参见下面的详细示例),就像在此操作中添加新的for
循环(双for
循环)。
哪种方法比较好?我们应该如何修改迭代器代码?
根据之前的玩具示例进行修改:
len1 = 4, len2 = 3; len3 = 2;
vecA.size() = len1 * len2 * len3;
vecB.size() = len1 * len3;
vecA = [1, 2, 3, 4, 1, 1, 1, 1, 2, 2, 2, 2, 3, 2, 1, 0, 1, 1, 1, 1, 2, 2, 2, 2]
vecB = [{1, 2, 3, 4} + {1, 1, 1, 1} + {2, 2, 2, 2}, {3, 2, 1, 0} + {1, 1, 1, 1} + {2, 2, 2, 2}]
= [4, 5, 6, 7, 6, 5, 4, 3]
型
与前面的例子不同,在这个例子中,我们在批次len3 = 2
中对vecB
进行了两次reduce sum。
首次更新
更新了简单for循环和Thrust迭代器的比较
我比较了 simple for
loop 和 the method(below)(由@paleonix提供)之间的性能差距。
这种比较可以帮助初学者了解差异,或者在面临相同需求时做出选择。
此测试在配备Tesla V100 GPU的服务器上进行(我们的目标是探索差距,因此机器并不重要)。
注意事项:
1.对于数据大小,我只是设置了一个粗略的基线,因为每个大小在测试中是不同的。更重要的是,我应该测试不同的大小(iidoEe.,不同的len1
,len2
等。)(太多的组合...)。如果我有时间的话,我以后会做的。
1.我没有多次测试以获得平均结果。
1.对于fancy iterators
和for_each_n
之间的比较,可能需要更大的数据大小来比较它们。
以下是一个粗略的比较:x1c 0d1x的数据
从这个比较中,我们可以看到:
1.“simple for
loop”方法比Thrust迭代器慢是意料之中的。特别是,当数据大小变得更大时,其性能显著下降。
1.在Reduce
中,for_each_n
比reduce_by_key_batch
慢。
第二次更新
更新了len1
大而len2
小时使用不同迭代器进行reduce的比较
回想一下@paleonix提到的结果是不同的:
对于len 1非常大的特殊情况(对于现代GPU来说足够的并行性),这可能意味着len 2相当小,可以使用另一种基于for_each的类似内核的解决方案,该解决方案仅对并行值进行求和并进行合并访问:
我做了一些实验,粗略比较的结果如表所示:
的
结果证实了@paleonix的评论。
第三次更新
更新了len1 * len3
较大时使用迭代器和for_each进行reduce sum的比较
在这种情况下,在vecB
中分批(即,len3
)执行归约求和。
的
的
1条答案
按热度按时间wmtdaxz31#
这个答案中所有关于性能的陈述都假设数据大小足够大,可以使用默认的
CUDA
设备后端/系统(相对于OMP
或TBB
)充分利用现代GPU和Thrust。批量填充和填充变换
对于用
1
s填充批处理的示例,可以只对thrust::transform
使用花哨的迭代器,但由于对齐问题和代码复杂性,这可能是个坏主意。相反,可以使用thrust::transform_if
,其中“模板”由一个简单的计数迭代器给出,所有逻辑都放入 predicate 函子中。为了得到一个批量填充,仍然需要另一个更复杂的迭代器来从输入向量(vecB
)中读取。字符串
或者,可以只使用
thrust::for_each
,而不使用任何复杂的花哨迭代器,这可能更容易阅读/理解,并且对于这些琐碎的并行操作来说,性能应该相当/相同(由于下面的示例代码中实现的内核融合,它可能比两个转换更快):型
批量序列-/Iota-/Counting-Iterator
批处理序列相对简单地实现为花哨的迭代器,甚至不应该复制到内存中,而是在需要它作为输入的下一个操作中使用(内核融合):
型
批量重复复制
批量重复稍微复杂一点,但基本形式与批量填充使用的花哨迭代器相同:
型
它也准备好在即将到来的操作中进行延迟计算。
批量缩减
这是最难的一个,至少在获得良好的性能方面。以下使用
thrust::reduce_by_key
的实现通常适用,但可能不是很快,因为置换迭代器会扰乱coalescing,这对带宽限制内核的性能非常重要。型
A_transpose_it
迭代器对vecA
中的元素进行置换,使得应该求和的元素在使用它的归约算法中看起来彼此相邻。因此, predicate functor/lambda比较“keys”(计数迭代器)看起来就像是这种情况。我不认为一个一般的,性能降低这个问题是可以实现的,目前的算法在推力。您可能会更幸运地使用实际上是为了容纳多维数据而编写的库,如MatX(C++ API)或cuTENSOR(C API)。
对于
len1
非常大的特殊情况(对于现代GPU来说足够的并行性),这可能意味着len2
相当小,可以使用另一种基于for_each
的类似内核的解决方案,该解决方案仅对并行值进行求和并进行合并访问:型
更新:批量批量缩减
看到OP的基准测试结果,这比我预期的还要清楚(10,000在GPU世界中并不是特别大),我会使用与之前减少相同的合并
for_each
策略。在这种情况下,假设len1 * len3
非常大,但理想情况下len1
也是32的倍数。即使不是这种情况,由于一般的复杂性/开销,它仍然可能比分段减少(reduce_by_key
)好得多。型
为了完整起见,我还在下面的完整代码中添加了一个使用
reduce_by key
的通用版本。完整源代码
使用
nvcc -extended-lambda -std=c++17
编译(在CUDA 12中,由于某种原因,我需要-rdc=True
)。型