在GPU上并行写入位集(数组的数组)

时间:2019-03-16 02:24:09

标签: algorithm cuda

以下是与我要加快的问题相关的GPU算法问题:

让我们假设我在概念上具有如下数据字段,其中512是该块中的线程数:

bool is_a_foo[131072][512];

此结构中的bool表示其他地方的数据(恰好具有相似的尺寸...但这无关紧要)是否为foo。为简单起见,我们假设我只是在一个GPU块上运行,每个线程都经过(通过__syncwarp()处于锁定步骤...但是请不要让它太分散注意力,因为实际上我m做一些更有意义的操作)0-> 131071。换句话说,每个线程的代码如下所示:

// assume is_a_foo is initialized earlier to 0's by some sort of memset call
// assume that the values for is_a_foo can go from false->true but never from true->false
for (int i = 0; i < 131072; ++i) {
    if (something_kind_of_expensive_but_not_the_bottleneck()) {
        is_a_foo[ i ][thread] = true;
    }
}

每个bool用8位表示,没有数据丢失。但是,让我们假设我想收紧内存/缓存的占用空间和带宽消耗。我们可以将上述数据结构表示为:

unsigned int is_a_foo[131072][512 / (sizeof(unsigned int) * 8)];

我们可以执行位算术将感兴趣的特定位设置为1。

问题在于,如果没有任何特殊处理,对is_a_foo的写入将相互粉碎,并且并非应将所有应设置为1的位都必须设置为1。

在我们愿意做一些特别的事情的情况下,我们可以使用atomicCAS来确保不会丢失任何写操作。不幸的是,这似乎有点昂贵。确实,在我的应用程序中,内核启动大约需要30毫秒,内核执行时间增加了约33%。目前尚不清楚额外时间是由于原子操作还是额外的指令所致,但我怀疑这是原子操作。

可以减轻损害的一件事是,如果我能够使用unsigned char而不是unsigned int进行操作。不幸的是,CUDA没有提供这种接口。而且,当我在unsigned short上进行操作时,收到有关该功能不适用于unsigned short的编译器错误(详细信息可应要求提供)。

所有这些都是要问的,有没有适合在GPU上进行此类操作的算法/数据结构

2 个答案:

答案 0 :(得分:2)

我不知道任何支持CUDA的扭曲大小为512的GPU,因此我假设您打算写块大小和__syncthreads()而不是扭曲大小和__syncwarp()(扭曲到目前为止,每个现有的CUDA架构的大小为32)。我也可以将您的注意力吸引到存在atomicOr()函数的事实。

为了最大程度地减少原子数量(或一般而言,全局内存流量),一种典型的方法是在您的块中执行parallel reduction(使用共享内存)以建立整个块的结果,然后仅最后使用一堆线程将结果移至全局内存。通常,我强烈建议您看一下CUB的库,该库提供各种并行编程原语(例如简化)的CUDA实现。但是,在您的特定情况下,同一扭曲内的线程可以使用__ballot()扭曲投票功能(映射到一条指令)简单地执行有问题的归约。由于在您的情况下计算得出的数字使得结果恰好是每个扭曲(32个线程)一个32位位掩码,因此您只需执行一个__ballot(),然后每个扭曲就有一个(例如第一个)线程写结果。如果我正确地理解了您的问题,那么您甚至不需要原子,因为结果似乎是每个块每个warp一个位掩码,这意味着一旦您每个线程只有一个线程访问全局内存,就不会同时访问同一位置。翘曲……

答案 1 :(得分:1)

您是否考虑过用其他方式打包您的位?如果int中的连续位属于2D数组的 first 组件而不是第二个组件,则可以从较低的内存占用量中受益,同时避免错误共享。

考虑结构:

static constexpr bits = sizeof(unsigned int) * 8;

class IsAFoo {
  private:
    static constexpr size = 131072/bits;
    unsigned int data[size][512];
  public:
    __host__ __device__ void set(int i, int thread, bool value) {
      unsigned int bit = 1u << (i%bits);
      if (value)
        data[i/bits][thread] |= bit;
      else
        data[i/bits][thread] &= ~(bit);
    }
    __host__ __device__ bool get(int i, int thread) {
      return bool(data[i/bits][thread] & (1u << (i%bits));
    }
}

__device__ IsAFoo is_a_foo;

...,然后算法的其余部分将像以前一样工作-您只需要使用上面的setget函数。显然,这假定您在程序中的其他任何地方都不会尝试使用其他模式来更改数组,例如set(threadIdx.x, commonValue)

此外,如果优化器很聪明,或者您需要进行一些手动调整,则可以显着减少主内存上的总操作次数。像这样:

unsigned int tmpFlags = 0;
for (int i = 0; i < 131072; ++i) {
    if (something_kind_of_expensive_but_not_the_bottleneck()) {
        tmpFlags |= 1u << (i % bits)
    }
    if (i % bits == bits - 1) {
        is_a_foo.setBulk(i, threadIdx.x, tmpFlags)
        tmpFlags = 0;
    }
}

(假设setBulk类中给出了IsAFoo)。这将使全局存储器操作的总数减少32倍,但需要增加一个单独的实时寄存器和一些算术运算。