使用Cuda并行实现大型阵列中大的连续子序列的总和的计算

时间:2017-02-14 08:46:12

标签: parallel-processing cuda nvidia gpgpu

我有1000个元素的大数组,我想使用CUDA计算这个大数组中大小为100的大连续子序列的总和。 这里是一个小尺寸的说明性例子。数组的大小为20,序列的大小为5.

tab = [80,12,14,5,70,9,26,30,8,12,16,15,60,12,38,32,17,67,19,11]

序列如下:

S1= 80+12+14+5+70
S2= 12+14+5+70+9
S3= 14+5+70+9+26
....

您是否有一个有效的想法,使用CUDA并行化1000个元素的数组和100的序列来并行化这个任务?

2 个答案:

答案 0 :(得分:3)

一些前言评论:

  1. 正如评论中已经提到的,总数组大小为1000是在GPU上运行的非常小问题。如果这是你在GPU上做的唯一事情,你可能不会从GPU代码中获得有趣的加速。
  2. previous answer建议使用一维模板。即使模板宽度为100(或200),仍然可以使用一维模板法。这里模板尺寸的唯一真正限制因素是共享存储器的大小,并且共享存储器可以容易地保持数据大小等于每个块的线程数+ 200用于模板(序列)"晕"。但是那个主题已经涵盖在那里了,所以我将介绍另一种能够处理任意数组长度和任意序列大小的方法。
  3. 在这种情况下,我们可以考虑是否可以利用prefix sum来帮助我们。前缀和存在快速并行方法,因此这使得它具有吸引力。前缀和只是表示输入序列中所有先前数字之和的数字输出序列。所以,如果我有这样的数组:

    1, 1, 3, 2, 0, 1
    

    独占前缀总和将是:

    0, 1, 2, 5, 7, 7, 8
    

    exclusive 此处表示当前总和位置包含所有先前的值,但排除当前位置的数据项。对于独占前缀总和,请注意我们可以(如果我们希望)生成"有用"序列的数据比输入序列长度长1。

    前缀总和可以很容易地适应您要求的问题。假设我的上述序列,我们想要计算3的子序列。我们可以取前缀和,并从中减去相同的前缀和序列,将移动3(序列长度),就像这样:

      0, 1, 2, 5, 7, 7, 8
    -          0, 1, 2, 5, 7, 7, 8
    =          5, 6, 5, 3
    

    在这种情况下,这个序列(5,6,5,3)将是期望的答案。

    以下是一个完整的例子,仅限于推力。我已经完成了它,因为在CUDA中编写快速并行前缀和并不是一件容易的事情,因此我更愿意使用(并建议其他人使用)实现。如果你想探索如何编写你自己的并行前缀和,你可以探索推力,这是开源,或阅读this作为介绍。

    这是一个功能齐全的例子:

    $ cat t1279.cu
    #include <thrust/device_vector.h>
    #include <thrust/scan.h>
    #include <thrust/transform.h>
    #include <thrust/functional.h>
    #include <thrust/copy.h>
    #include <iostream>
    
    const int arr_size =  1000;
    const int seq_len  =   100;
    
    typedef int mytype;
    
    int main(){
    
      // test case
      mytype t_arr[] = {80,12,14,5,70,9,26,30,8,12,16,15,60,12,38,32,17,67,19,11,0};
      int t_arr_size = sizeof(t_arr)/sizeof(mytype);
      int t_seq_len = 5;
      thrust::device_vector<mytype> d_arr1(t_arr, t_arr+t_arr_size);
      thrust::device_vector<mytype> d_res1(t_arr_size);
      thrust::device_vector<mytype> d_out1(t_arr_size-t_seq_len);
    
      thrust::exclusive_scan(d_arr1.begin(), d_arr1.end(), d_res1.begin());
      thrust::transform(d_res1.begin()+t_seq_len, d_res1.end(), d_res1.begin(), d_out1.begin(), thrust::minus<mytype>());
      thrust::copy_n(d_out1.begin(), t_arr_size-t_seq_len, std::ostream_iterator<mytype>(std::cout, ","));
      std::cout << std::endl;
    
      // case with larger array length and larger sequence length
      thrust::device_vector<mytype> d_arr(arr_size+1, 1);
      thrust::device_vector<mytype> d_res(arr_size+1);
      thrust::device_vector<mytype> d_out(arr_size+1-seq_len);
    
      thrust::inclusive_scan(d_arr.begin(), d_arr.end(), d_res.begin());
      thrust::transform(d_res.begin()+seq_len, d_res.end(), d_res.begin(), d_out.begin(), thrust::minus<mytype>());
      // validate
      for (int i = 0; i < arr_size+1-seq_len; i++) {
        mytype t = d_out[i];
        if (t != seq_len) {std::cout << "mismatch at: " << i << "was: " << t << "should be: " << seq_len << std::endl; return 1;}
        }
      return 0;
    }
    
    $ nvcc -arch=sm_35 -o t1279 t1279.cu
    $ ./t1279
    181,110,124,140,143,85,92,81,111,115,141,157,159,166,173,146,
    $
    

答案 1 :(得分:1)

非常相似的问题:Parallel implementation of the computation of the sum of contiguous subsequences in an array using Cuda

罗伯特克罗维拉回答说。

之前回答了序列大小= 4,现在这仅仅是大小= 100。要么你的内核适应这个类似的问题,要么你在cpu上解决这个问题,因为cpu应该能够在一个单独的1000-10000个周期内解决这个问题(N = 1000,L = 100)(复杂度为O(N))核心虽然gpu的许多周期都是未使用的,但只有高级的gpu可以进行负载平衡以使用这些空闲资源。 CPU甚至可以在将数组发送到gpu之前解决这个问题。但是如果必须在GPU(两个内核之间)上发生这种情况,那么我可以建议Robert Crovella的答案中仅与硬件相关的优化,例如:

  • 从纹理缓存和/或常量内存中获取一半数组,以进一步增加计算资源的内存带宽(在共享/全局内存之上)。(也必须能够增加每个计算单元的可用大小/ smx这种方式)
  • 使用多个临时累加器而不是一个,以增加编译器执行某些指令优化的机会(指令级并行性?)
  • 如果FP64资源的数量与FP32资源相当,则使用双精度(或单精度)添加更多性能,但仅在每个第N个线程组中,其中N类似于2 * total_FP32_units / total_FP64_units,并且仅当存在多个时每smx warp以确保正在使用FP32和FP64资源。也许其中一个累加器可能是FP64,而不是在整个组中使用FP64。(但这会隐藏更多的转换延迟)
  • 不仅计算第i个索引,而且还计算i + 1st和i + 2nd索引,每个线程,在计算第i个线程后,每个线程只需要4个额外的添加而不是200个(在另外两个线程上)一。例如,添加下一个元素和减去最旧的元素。为什么每个线程3个工作?保持所有内存条忙,而不是进行内存条冲突(如果共有2,4,8,10个内存条)。如果有3,6,9个内存库,那么它可能是2个或4个作业线程。但这会增加(双倍,三倍)临时内存需求,因此必须有一个均衡的性能点,可以对某些架构进行基准测试以了解它。

我不知道cuda,但它应该能够使用多个内存限定符来使用更多的gpu芯片区域来更快地提供数据,就像opencl一样。

如果共享内存和全局内存有不同/单独的管道到达流处理器,那么你甚至可以使用全局内存来完成某些工作(我的意思是,不要将所有内容加载到共享,而是将一些内容从全局加载而不是让它使用所有shared_lines + global_lines)

32M元素阵列可能无法从第一次优化中获得帮助,但您仍然可以使用FP64累加器来拥有多个临时累加器(例如,每个线程使用8个FP32累加器和1个FP64累加器(使用FMA来减少其延迟,这可能是隐藏在这8个累加器后面,所以整个数组元素被同等对待)但是如果常量内存或纹理缓存有50000个元素的空间,它可以服务250个组(每个有200个元素来自这种类型的内存)或1250个组(每个都有50来自这个内存,150来自其他(共享,...))并且仍然可以稍微提高性能。(例如,100k元素阵列需要1k组,%25个线程使用常量内存)