【问题标题】:Cuda thrust global memory writing very slowCuda 推动全局内存写入非常慢
【发布时间】:2012-12-09 03:01:54
【问题描述】:

我目前正在编写一个代码,它使用 Nvidia 推力库计算 GPU 上的积分直方图。

因此我分配了一个连续的设备内存块,我一直使用自定义函子进行更新。

问题是,写入设备内存非常慢,但读取实际上还可以。

基本设置如下:

struct HistogramCreation
{
    HistogramCreation(
    ...
    // pointer to memory
    ...
    ){}

    /// The actual summation operator
    __device__ void operator()(int index){
       .. do the calculations ..
       for(int j=0;j<30;j++){

       (1)  *_memoryPointer =  values (also using reads to such locations) ;

       }
  }
}

void foo(){

  cudaMalloc(_pointer,size);

  HistogramCreation initialCreation( ... _pointer ...);
  thrust::for_each(
    thrust::make_counting_iterator(0),
    thrust::make_counting_iterator(_imageSize),
    initialCreation);
}

如果我将(1)中的写法更改为以下内容>

unsigned int val = values;

性能要好得多。这是我唯一的全局内存写入。

使用内存写入我得到大约 2 秒的高清素材。 使用局部变量大约需要 50 毫秒,因此减少了大约 40 倍。

为什么这么慢?我该如何改进它?

【问题讨论】:

  • 这不是您问题的答案,但我最近需要一个累积直方图,我发现 this example code 很有帮助。如果您还没有看过它,可能会感兴趣。

标签: performance memory cuda thrust


【解决方案1】:

正如@OlegTitov 所说,使用全局频繁加载/存储 应尽可能避免记忆。当有一个 不可避免的情况,然后合并记忆 访问可以帮助执行过程不会变得太慢; 但是在大多数情况下,直方图计算非常困难 实现合并访问。

虽然以上大部分内容基本上只是重申 @OlegTitov 的回答,我只想分享一个 我对 NVIDIA 求和的调查 CUDA。实际上结果很有趣,我希望 对于其他 xcuda 开发人员来说,这将是一个有用的信息。

这个实验基本上是为了运行一个寻找速度的测试 各种内存访问模式的求和:使用全局 内存(1 个线程)、L2 缓存(原子操作 - 128 个线程)和 L1 缓存(共享内存 - 128 个线程)

本实验使用: 开普勒 GTX 680, 1546 核 @ 1.06GHz GDDR5 256 位 @ 3GHz

这是内核:

__global__
void glob(float *h) {
    float* hist = h;
    uint sd = SEEDRND;
    uint random;
    for (int i = 0; i < NUMLOOP; i++) {
        if (i%NTHREADS==0) random = rnd(sd);
        int rind = random % NBIN;
        float randval = (float)(random % 10)*1.0f ;
        hist[rind] += randval;
    }
}

__global__
void atom(float *h) {
    float* hist = h;
    uint sd = SEEDRND;
    for (int i = threadIdx.x; i < NUMLOOP; i+=NTHREADS) {
        uint random = rnd(sd);
        int rind = random % NBIN;
    float randval = (float)(random % 10)*1.0f ;
        atomicAdd(&hist[rind], randval);
    }
}

__global__
void shm(float *h) {
    int lid = threadIdx.x;
    uint sd = SEEDRND;

    __shared__ float shm[NTHREADS][NBIN];
    for (int i = 0; i < NBIN; i++) shm[lid][i] = h[i];

    for (int i = lid; i < NUMLOOP; i+=NTHREADS) {
        uint random = rnd(sd);
        int rind = random % NBIN;
        float randval = (float)(random % 10)*1.0f ;
        shm[lid][rind] += randval;
    }

    /* reduction here */
    for (int i = 0; i < NBIN; i++) {
        __syncthreads();
        if (threadIdx.x < 64) {
            shm[threadIdx.x][i] += shm[threadIdx.x+64][i];
        }
        __syncthreads();
        if (threadIdx.x < 32) {
            shm[threadIdx.x][i] += shm[threadIdx.x+32][i];
        }
        __syncthreads();
        if (threadIdx.x < 16) {
            shm[threadIdx.x][i] += shm[threadIdx.x+16][i];
        }
        __syncthreads();
        if (threadIdx.x < 8) {
            shm[threadIdx.x][i] += shm[threadIdx.x+8][i];
        }
        __syncthreads();
        if (threadIdx.x < 4) {
            shm[threadIdx.x][i] += shm[threadIdx.x+4][i];
        }
        __syncthreads();
        if (threadIdx.x < 2) {
            shm[threadIdx.x][i] += shm[threadIdx.x+2][i];
        }
        __syncthreads();
        if (threadIdx.x == 0) {
            shm[0][i] += shm[1][i];
        }
    }

    for (int i = 0; i < NBIN; i++) h[i] = shm[0][i];
}

输出

atom:  102656.00 shm:  102656.00 glob:  102656.00
atom:  122240.00 shm:  122240.00 glob:  122240.00
... blah blah blah ...

  One Thread: 126.3919 msec
      Atomic:   7.5459 msec
      Sh_mem:   2.2207 msec

这些内核之间的比例为 57:17:1。很多东西可以 在这里进行分析,并不真正意味着使用 L1 或 L2 内存空间总是会给你超过 10 个 整个程序的加速倍数。

以下是主要功能和其他功能:

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;

#define NUMLOOP 1000000
#define NBIN 36
#define SEEDRND 1

#define NTHREADS 128
#define NBLOCKS 1

__device__ uint rnd(uint & seed) {
#if LONG_MAX > (16807*2147483647)
    int const a    = 16807;
    int const m    = 2147483647;
    seed = (long(seed * a))%m;
    return seed;
#else
    double const a    = 16807;
    double const m    = 2147483647;

    double temp = seed * a;
    seed = (int) (temp - m * floor(temp/m));
    return seed;
#endif
}

... the above kernels ...

int main()
{
    float *h_hist, *h_hist2, *h_hist3, *d_hist, *d_hist2,
    *d_hist3;
    h_hist = (float*)malloc(NBIN * sizeof(float));
    h_hist2 = (float*)malloc(NBIN * sizeof(float));
    h_hist3 = (float*)malloc(NBIN * sizeof(float));
    cudaMalloc((void**)&d_hist, NBIN * sizeof(float));
    cudaMalloc((void**)&d_hist2, NBIN * sizeof(float));
    cudaMalloc((void**)&d_hist3, NBIN * sizeof(float));

    for (int i = 0; i < NBIN; i++) h_hist[i] = 0.0f;
    cudaMemcpy(d_hist, h_hist, NBIN * sizeof(float),
    cudaMemcpyHostToDevice);
    cudaMemcpy(d_hist2, h_hist, NBIN * sizeof(float),
    cudaMemcpyHostToDevice);
    cudaMemcpy(d_hist3, h_hist, NBIN * sizeof(float),
    cudaMemcpyHostToDevice);

    cudaEvent_t start, end;
    float elapsed = 0, elapsed2 = 0, elapsed3;
    cudaEventCreate(&start);
    cudaEventCreate(&end);

    cudaEventRecord(start, 0);

    atom<<<NBLOCKS, NTHREADS>>>(d_hist);
    cudaThreadSynchronize();

    cudaEventRecord(end, 0);
    cudaEventSynchronize(start);
    cudaEventSynchronize(end);
    cudaEventElapsedTime(&elapsed, start, end);

    cudaEventRecord(start, 0);

    shm<<<NBLOCKS, NTHREADS>>>(d_hist2);
    cudaThreadSynchronize();

    cudaEventRecord(end, 0);
    cudaEventSynchronize(start);
    cudaEventSynchronize(end);
    cudaEventElapsedTime(&elapsed2, start, end);

    cudaEventRecord(start, 0);

    glob<<<1, 1>>>(d_hist3);
    cudaThreadSynchronize();

    cudaEventRecord(end, 0);
    cudaEventSynchronize(start);
    cudaEventSynchronize(end);
    cudaEventElapsedTime(&elapsed3, start, end);

    cudaMemcpy(h_hist, d_hist, NBIN * sizeof(float),
    cudaMemcpyDeviceToHost);
    cudaMemcpy(h_hist2, d_hist2, NBIN * sizeof(float),
    cudaMemcpyDeviceToHost);
    cudaMemcpy(h_hist3, d_hist3, NBIN * sizeof(float),
    cudaMemcpyDeviceToHost);

    /* print output */
    for (int i = 0; i < NBIN; i++) {
        printf("atom: %10.2f shm: %10.2f glob:
    %10.2f¥n",h_hist[i],h_hist2[i],h_hist3[i]);
    }

    printf("%12s: %8.4f msec¥n", "One Thread", elapsed3);
    printf("%12s: %8.4f msec¥n", "Atomic", elapsed);
    printf("%12s: %8.4f msec¥n", "Sh_mem", elapsed2);

    return 0;
}

【讨论】:

    【解决方案2】:

    在编写 GPU 代码时,您应该避免读写全局内存。 GPU 上的全局内存非常慢。这就是硬件功能。您唯一能做的就是使相邻的踏板在全局内存中的相邻地址中读/写。这将导致合并并加速该过程。但一般来说,读取一次数据,处理一次,然后写出一次。

    【讨论】:

    • 在我的情况下阅读仍然非常快,只是写作很慢。问题是我真的需要全局内存,因为原则上一个像素将由任意其他位置(当然在设备上)更新。由于写入位置很慢,但读取速度很快,我假设某种形式的锁定/序列化机制我不需要,因为我没有竞争条件,一切都可以自己行动。
    • @user1913946 您正在执行单次读取和 30 次写入...或者您跳过了一些代码行?在那种情况下,您可以显示for循环的代码吗?
    • 在 for 循环中实际上有 *_p1++ = *_p2++ + *_p3++ + *_p4++ *_p5++ 形式的东西,因此每次迭代 4 次读取和 1 次写入。如果我用局部变量交换 *_p1++ 效果会出现
    • 如果您的 GPU 具有 >= 2.0 的计算能力,那么您就有了用于全局内存的缓存,并且效果可能归因于它。当您仅从全局内存中读取时,您可能会缓存命中并且一切正常。当您开始写入时,它可能会使缓存行无效并强制进行实际读取。但我不确定是否是这种情况。尝试更改算法。每个线程 150 次内存访问几乎没有计算是非常奇怪的。
    • 正如我所说,这不是完整的代码,而是对问题真正重要的所有内容。如果您计算图像的积分直方图,则必须分阶段进行如果您想获得良好的并行化。首先,我计算每个大小为 1 的单个像素的直方图,直到我得到所有像素的直方图。它总共所做的只是每个线程大约 log(n)*150 次访问,这应该比两秒更快。
    【解决方案3】:

    请注意,在您进行修改后,NVCC 可能会优化您的大量代码 - 它检测到没有写入全局内存,只是删除“不需要的”代码。因此,这种加速可能不会来自全球作家。

    我建议在您的实际代码(具有全局写入功能的代码)上使用分析器,以查看是否存在未对齐访问或其他性能问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-01-10
      • 2016-09-21
      • 1970-01-01
      • 2016-05-23
      • 1970-01-01
      • 2013-11-09
      • 1970-01-01
      相关资源
      最近更新 更多