【问题标题】:Using shared memory in CUDA without reducing threads在 CUDA 中使用共享内存而不减少线程
【发布时间】:2012-05-04 08:52:50
【问题描述】:

查看 Mark Harris 的归约示例,我想看看是否可以让线程存储中间值而无需归约操作:

例如CPU代码:

for(int i = 0; i < ntr; i++)
{
    for(int j = 0; j < pos* posdir; j++)
    {
        val = x[i] * arr[j];
        if(val > 0.0)
        {
            out[xcount] = val*x[i];
            xcount += 1;
        }
    }
}

等效的 GPU 代码:

const int threads = 64; 
num_blocks = ntr/threads;

__global__ void test_g(float *in1, float *in2, float *out1, int *ct, int posdir, int pos)
{
    int tid = threadIdx.x + blockIdx.x*blockDim.x;
    __shared__ float t1[threads];
    __shared__ float t2[threads];

    int gcount  = 0;

    for(int i = 0; i < posdir*pos; i += 32) {
        if (threadIdx.x < 32) {
            t1[threadIdx.x] = in2[i%posdir];
        }
       __syncthreads();

        for(int i = 0; i < 32; i++)
        {
            t2[i] = t1[i] * in1[tid];
                if(t2[i] > 0){
                    out1[gcount] = t2[i] * in1[tid];
                    gcount = gcount + 1;
                }
        }
    }        
    ct[0] = gcount;
}

我在这里尝试做的是以下步骤:

(1)将in2的32个值存储在共享内存变量t1中,

(2)对于i和in1[tid]的每一个值,计算t2[i],

(3)if t2[i] &gt; 0 对于 i 的特定组合,将t2[i]*in1[tid] 写入out1[gcount]

但我的输出全错了。我什至无法计算 t2[i] 大于 0 的所有时间。

关于如何为每个 i 和 tid 保存 gcount 值的任何建议?在调试时,我发现对于块 (0,0,0) 和线程 (0,0,0),我可以按顺序看到 t2 的值已更新。在 CUDA 内核将焦点切换到 block(0,0,0) 和 thread(32,0,0) 后,out1[0] 的值再次被重写。如何获取/存储每个线程的 out1 的值并将其写入输出?

到目前为止,我尝试了两种方法:(由 NVIDIA 论坛上的@paseolatis 建议)

(1) 定义offset=tid*32; and replace out1[gcount] with out1[offset+gcount],

(2) 定义

__device__ int totgcount=0; // this line before main()
atomicAdd(&totgcount,1);
out1[totgcount]=t2[i] * in1[tid];

int *h_xc = (int*) malloc(sizeof(int) * 1);
cudaMemcpyFromSymbol(h_xc, totgcount, sizeof(int)*1, cudaMemcpyDeviceToHost);
printf("GPU: xcount = %d\n", h_xc[0]); // Output looks like this: GPU: xcount = 1928669800

有什么建议吗?提前致谢 !

【问题讨论】:

  • 您能否提供一些ntrposidrpos 的典型值?另外,一个典型的计算会产生多少个输出点(CPU 代码中的xcount)?
  • @talonmies:我尝试了以下值:ntr = 128、pos = 1 和 posdir = 32。
  • @talonmies:memcpyfrom 符号是正确的方法吗??
  • 是的。希望你有一个 cudaMemcpyToSymbol 调用为零 gcount 如果你以原子方式访问它。
  • 如何将cudaMemcpySymbol 归零gcount ??我知道你可以在内核启动之前显式设置gcount = 0

标签: c cuda shared-memory


【解决方案1】:

好的,让我们将您对代码应该做什么的描述与您发布的内容进行比较(有时称为rubber duck debugging)。

  1. 在共享内存变量t1中存储in2的32个值

    您的内核包含以下内容:

    if (threadIdx.x < 32) {
        t1[threadIdx.x] = in2[i%posdir];
    }
    

    这实际上是将in2中的相同值加载到t1的每个值中。我怀疑你想要更多这样的东西:

    if (threadIdx.x < 32) {
        t1[threadIdx.x] = in2[i+threadIdx.x];
    }
    
  2. 对于i和in1[tid]的每个值,计算t2[i]

    这部分没问题,但为什么共享内存中需要t2?它只是内部迭代完成后可以丢弃的中间结果。你可以很容易地得到类似的东西:

    float inval = in1[tid];
    .......
    for(int i = 0; i < 32; i++)
    {
         float result = t1[i] * inval;
         ......
    
  3. 如果t2[i] &gt; 0 表示 i 的特定组合,请写 t2[i]*in1[tid]out1[gcount]

    这才是问题真正开始的地方。在这里你可以这样做:

            if(t2[i] > 0){
                out1[gcount] = t2[i] * in1[tid];
                gcount = gcount + 1;
            }
    

    这是一场记忆竞赛。 gcount 是一个线程局部变量,因此每个线程将在不同的时间用自己的值覆盖任何给定的out1[gcount]。为了让这段代码能够正常工作,您必须拥有gcount 作为全局内存变量并使用原子内存更新来确保每个线程在每次输出值时都使用唯一的gcount 值。但是请注意,如果经常使用原子内存访问会非常昂贵(这就是为什么我在评论中询问每次内核启动有多少输出点)。

生成的内核可能如下所示:

__device__ int gcount; // must be set to zero before the kernel launch

__global__ void test_g(float *in1, float *in2, float *out1, int posdir, int pos)
{
    int tid = threadIdx.x + blockIdx.x*blockDim.x;
    __shared__ float t1[32];

    float ival = in1[tid];

    for(int i = 0; i < posdir*pos; i += 32) {
        if (threadIdx.x < 32) {
            t1[threadIdx.x] = in2[i+threadIdx.x];
        }
        __syncthreads();

        for(int j = 0; j < 32; j++)
        {
            float tval = t1[j] * ival;
            if(tval > 0){
                int idx = atomicAdd(&gcount, 1);
                out1[idx] = tval * ival
            }
        }
    }        
}

免责声明:用浏览器编写,从未编译或测试过,使用风险自负.....

请注意,您对ct 的写入也是内存争用,但gcount 现在是一个全局值,您可以在内核之后读取该值而无需ct


编辑:在运行内核之前,您似乎在将gcount 归零时遇到了一些问题。为此,您需要使用cudaMemcpyToSymbolcudaGetSymbolAddresscudaMemset 之类的东西。它可能看起来像:

const int zero = 0;
cudaMemcpyToSymbol("gcount", &zero, sizeof(int), 0, cudaMemcpyHostToDevice);

再次声明,通常的免责声明:用浏览器编写,从未编译或测试过,使用风险自负.....

【讨论】:

  • 感谢t1[threadIdx.x] = in2[i%posdir]; 的反馈但我想这样做是因为在大多数情况下 pos*posdir 将在 32 和 2048 之间。目前我正在运行这个 ntr=128 并且执行时间是615 微秒。通常 ntr 的值约为 200K,所以我将使用 atomicAdds 运行这段代码,看看时间缩放是如何工作的。此外,与 gcount 的竞争条件仍然存在。我看到 gcount 的值为 1928669800。
  • 我在内核执行后这样做了:int *h_xc = (int*) malloc(sizeof(int) * 1); cudaMemcpyFromSymbol(h_xc, gcount, sizeof(int)*1, cudaMemcpyDeviceToHost); printf("GPU: xcount = %d\n", h_xc[0]); // Output looks like this: GPU: xcount = 1928669800
  • 我尝试了不同的配置:ntr=204800 和 posdir=1024,800 个块和 256 个线程的 GPU 执行时间 = 21.322 ms。 (不确定这是否好,但它比 1.035 秒的 CPU 时间要好)
  • 另外,我做了这个更改:int idx = atomicAdd(&amp;gcount, 1); 出现编译器错误
  • @cuda_hpc80:你注意到第一行旁边的评论了吗?在运行内核之前,您必须将 gcount 显式归零。也很抱歉语法错误,我在浏览器中编写了这段代码,忘记添加我常用的cavaet。
【解决方案2】:

更好的方法是为每个线程提供自己的输出,并让它增加自己的count 并输入值 - 这样,双 for 循环可以以任何顺序并行发生,这就是 GPU 做得好的地方。输出是错误的,因为线程共享 out1 数组,所以它们都会覆盖它。

您还应该将要复制到共享内存中的代码移动到一个单独的循环中,并在后面加上__syncthreads()。随着__syncthreads() 退出循环,您应该获得更好的性能 - 这意味着您的共享数组必须是 in2 的大小 - 如果这是一个问题,那么在结束时有更好的方法来处理这个问题回答。

您还应该将threadIdx.x &lt; 32 支票移到外面。所以你的代码看起来像这样:

if (threadIdx.x < 32) {
    for(int i = threadIdx.x; i < posdir*pos; i+=32) {
        t1[i] = in2[i];
    }
}
__syncthreads();

for(int i = threadIdx.x; i < posdir*pos; i += 32) {
    for(int j = 0; j < 32; j++)
    {
         ...
    }
}

然后放一个__syncthreads(),一个原子加法gcount += count,然后从本地输出数组复制到一个全局数组——这部分是顺序的,会影响性能。如果可以的话,我会为每个本地数组提供一个指向数组的全局指针列表,然后将它们放在 CPU 上。

另一个变化是 t2 不需要共享内存 - 它对您没有帮助。而你这样做的方式,它似乎只有在你使用单个块时才有效。要从大多数 NVIDIA GPU 中获得良好的性能,您应该将其划分为多个块。您可以根据您的共享内存限制对其进行调整。当然,块之间没有__syncthreads(),因此每个块中的线程必须遍历内部循环的整个范围,以及外部循环的分区。

【讨论】:

  • 据我所知,该重构并没有完成原始代码试图做的事情。但是您对gcount 上的原子添加需求是正确的。
猜你喜欢
  • 2013-09-22
  • 2016-06-08
  • 2014-10-01
  • 2011-12-31
  • 2016-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-29
相关资源
最近更新 更多