根据CUDA中的索引进行流压缩和转换

时间:2016-01-14 13:00:28

标签: algorithm optimization cuda stream-compaction

我的设备上有一个float数组,我想执行一个stram压缩操作(如此处所示:http://http.developer.nvidia.com/GPUGems3/gpugems3_ch39.html),然后根据值和地址或原始应用变换元件。

例如,我有一个值为{10,-1,-10,2}的数组,我希望返回绝对值大于5的所有元素,并应用一个转换值和它的值数组中的地址。 这里的结果是{transform(10,0),transform(-10,2)}。

我试图使用推力,但这是一个经常在大型数组上运行的代码,所以理想情况下它不会使用缓冲区和多次遍历数组。

在不分配辅助数组和进行多次遍历的情况下,是否可以做我想做的事情?如果是的话,这些代码是否存在?或者至少有没有人指出推力的功能或我可以编写的任何其他库来实现我的目标?

1 个答案:

答案 0 :(得分:4)

是的,可以使用单推力算法调用(我假设你的意思是“没有......进行多次遍历”)并且没有“分配辅助数组”。

一种方法是将数据数组加上一个索引/“地址”数组(通过thrust::counting_iterator,避免分配)传递给创建“转换”操作的thrust::transform_iterator(结合一个合适的函子)。

然后,您可以将上述转换迭代器传递给适当的thrust stream compaction algorithm,以选择所需的值。

这是一种可行的方法:

$ cat t1044.cu
#include <thrust/device_vector.h>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/iterator/transform_iterator.h>
#include <thrust/iterator/zip_iterator.h>
#include <thrust/copy.h>
#include <math.h>

#include <iostream>

__host__ __device__ int my_transform(int data, int idx){
  return (data - idx);  //put whatever transform you want here
}

struct my_transform_func : public thrust::unary_function<thrust::tuple<int, int>, int>
{

  __host__ __device__
  int operator()(thrust::tuple<int, int> &t){
    return my_transform(thrust::get<0>(t), thrust::get<1>(t));
    }
};

struct my_test_func
{
  __host__ __device__
  bool operator()(int data){
    return (abs(data) > 5);
    }
};



int main(){

  int data[] = {10,-1,-10,2};
  int dsize = sizeof(data)/sizeof(int);

  thrust::device_vector<int> d_data(data, data+dsize);
  thrust::device_vector<int> d_result(dsize);
  int rsize = thrust::copy_if(thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple(d_data.begin(), thrust::counting_iterator<int>(0))), my_transform_func()), thrust::make_transform_iterator(thrust::make_zip_iterator(thrust::make_tuple(d_data.end(), thrust::counting_iterator<int>(dsize))), my_transform_func()), d_data.begin(), d_result.begin(), my_test_func()) - d_result.begin();
  thrust::copy_n(d_result.begin(), rsize, std::ostream_iterator<int>(std::cout, ","));
  std::cout << std::endl;
  return 0;
}
$ nvcc -o t1044 t1044.cu
$ ./t1044
10,-12,
$

对这种方法的一些可能的批评:

  1. 它似乎是加载d_data个元素两次(一次用于转换操作,一次用于模板)。但是,CUDA优化编译器可能会识别最终生成的线程代码中的冗余负载,并对其进行优化。

  2. 我们似乎正在对每个数据元素执行转换操作,无论我们是否打算在结果中保存它。再次,推力copy_if实施可能实际上推迟数据加载操作,直到做出模板决定。如果是这种情况,则可能只根据需要进行转换。即使总是这样做,这可能是一个微不足道的问题,因为许多推力操作往往是加载/存储或内存带宽限制,而不是计算限制。然而,一个有趣的替代方法可能是使用@ m.s创建的自适应。 here创建一个应用于输出迭代器步骤的转换,这可能会将转换操作限制为仅对结果中实际保存的数据元素执行,尽管我有没有仔细检查过。

  3. 正如下面的评论中所提到的,这种方法确实分配了临时存储(作为copy_if操作的一部分,引力是如此),当然我明确地分配了O(n)存储结果。我怀疑推力分配(单cudaMalloc)也可能是O(n)存储。虽然有可能完成所有要求的事情(并行前缀总和,流压缩,数据转换),绝对没有任何类型的额外存储(因此请求可能是就地操作),我认为制作一个算法可能会产生显着的负面性能影响,如果它完全可行的话(我不清楚并行前缀和可以实现完全没有任何类型的额外存储,更不用说将它与流压缩耦合即数据并行运动)。由于推力释放了它使用的所有这种临时存储,因此频繁使用此方法不会有太多的存储问题。唯一剩下的问题(我猜)是表现。如果性能是一个问题,那么通过将上述算法与thrust custom allocator(也见here)相结合,应该主要消除与临时分配相关的时间开销,这将分配所需的最大存储缓冲区一次,然后每次使用上述算法时重新使用该缓冲区。