共享内存的DRAM利用率低

时间:2013-07-22 20:48:49

标签: cuda

我在GPU上实现了一个简单的几何布朗运动。 我的代码运行良好,即给出正确的值。我关心的是我正在加速的速度,我期待更多。 到目前为止,我有2个实现只能访问全局内存,速度提高约3倍,第二个使用共享内存,速度提高约2.3倍。

在使用Nvidia Visual Profiler分析应用程序之后,我的问题出现了。根据它,我的加载/存储效率为100%,但DRAM利用率非常低(约10%),全球存储器重放率接近50%,因为非合并访问。

有一次我看到我试图使用共享内存来避免全局内存访问,但令我惊讶的是DRAM降低了(4.5%),全局内存重放到46.3%

我注意到我的内核启动占用率很低,因为我几乎每块使用所有可用的共享内存,但我不知道这是否可以解释第二种方法的性能更差。

你能就性能方面的情况提出一些建议,可能会在哪些方面/我可以寻求什么来改善呢?

CUDA_IMPLEMENTATION.CU

#define BLOCK_SIZE  64

#define SHMEM_ROWS  7       //The same as c_numTimeSteps = numTimeSteps
#define SHMEM_COLS  BLOCK_SIZE

__constant__ double c_c1;
__constant__ double c_c2;
__constant__ int c_numTimeSteps;
__constant__ int c_numPaths;
__constant__ double c_timeNodes[2000];

__global__
void kernelSharedMem(double *rv, double *pb)
{
    __shared__ double sh_rv[SHMEM_ROWS*SHMEM_COLS];
    __shared__ double sh_pb[(SHMEM_ROWS+1)*SHMEM_COLS];

    int p = blockDim.x * blockIdx.x + threadIdx.x;

    //The idea of this outter loop is to have tiles along the rows
    for(int tb = 0; tb < c_numTimeSteps; tb += SHMEM_ROWS)
    {
        //Copy values into shared memory
        for(int is = tb, isSh = 0;
            is < tb+SHMEM_ROWS && is < c_numTimeSteps;
            is++, isSh++)
        {
            sh_rv[isSh*SHMEM_COLS+threadIdx.x] = 
                rv[is*c_numPaths+p];
        }

        sh_pb[threadIdx.x] = pb[tb*numPaths+p];

        __syncthreads();

        //Main computation in SHARED MEMORY
        for(int isSh = 0; isSh < SHMEM_ROWS; isSh++)
        {
            double dt = c_timeNodes[isSh];
            double sdt = sqrt(dt) * c_c1;
            double mdt = c_c2 * dt;

            sh_pb[(isSh+1)*SHMEM_COLS+threadIdx.x] =
                sh_pb[isSh*SHMEM_COLS+threadIdx.x] *
                exp(mdt + sdt * rv[isSh*SHMEM_COLS+threadIdx.x]);

        }

        __syncthreads();

        for(int is = tb, isSh = 0;
            is < tb+SHMEM_ROWS && is < c_numTimeSteps;
            is++, isSh++)
        {
            pb[(is+1)*c_numPaths+p] = 
                sh_pb[(isSh+1)*SHMEM_COLS+threadIdx.x];
        }

    }

}

__global__
void kernelGlobalMem(double *rv, double *pb)
{
    int p = blockDim.x * blockIdx.x + threadIdx.x;

    for(int i = 0; i < c_numTimeSteps; i++)
    {
        double dt = c_timeNodes[i];
        double sdt = sqrt(dt) * c_c1;
        double mdt = c_c2 * dt;

        pb[(i+1)*c_numPaths+p] = 
            pb[i*c_numPaths+p] *
            exp(mdt + sdt * rv[i*c_numPaths+p]);

    }

}

extern "C" void computePathGpu(vector<vector<double>>* rv,
                                vector<vector<double>>* pb,
                                int numTimeSteps, int numPaths,
                                vector<double> timeNodes,
                                double c1, double c2)
{

    cudaMemcpyToSymbol(c_c1, &c1, sizeof(double));
    cudaMemcpyToSymbol(c_c2, &c2, sizeof(double));
    cudaMemcpyToSymbol(c_numTimeSteps, &numTimeSteps, sizeof(int));
    cudaMemcpyToSymbol(c_numPaths, &numPaths, sizeof(int));
    cudaMemcpyToSymbol(c_timeNodes, &(timeNodes[0]), sizeof(double)*numTimeSteps);

    double *d_rv;
    double *d_pb;

    cudaMalloc((void**)&d_rv, sizeof(double)*numTimeSteps*numPaths);
    cudaMalloc((void**)&d_pb, sizeof(double)*(numTimeSteps+1)*numPaths);

    vector<vector<double>>::iterator itRV;
    vector<vector<double>>::iterator itPB;

    double *dst = d_rv;
    for(itRV = rv->begin(); itRV != rv->end(); ++itRV)
    {
        double *src = &((*itRV)[0]);
        size_t s = itRV->size();
        cudaMemcpy(dst, src, sizeof(double)*s, cudaMemcpyHostToDevice);
        dst += s;
    }

    cudaMemcpy(d_pb, &((*(pb->begin()))[0]),
        sizeof(double)*(pb->begin())->size(), cudaMemcpyHostToDevice);

    dim3 block(BLOCK_SIZE);
    dim3  grid((numPaths+BLOCK_SIZE-1)/BLOCK_SIZE);

    kernelGlobalMem<<<grid, block>>>(d_rv, d_pb);
    //kernelSharedMem<<<grid, block>>>(d_rv, d_pb);
    cudaDeviceSynchronize();

    dst = d_pb;
    for(itPB = ++(pb->begin()); itPB != pb->end(); ++itPB)
    {
        double *src = &((*itPB)[0]);
        size_t s = itPB->size();
        dst += s;
        cudaMemcpy(src, dst, sizeof(double)*s, cudaMemcpyDeviceToHost);
    }

    cudaFree(d_pb);
    cudaFree(d_rv);

}

的main.cpp

extern "C" void computeOnGPU(vector<vector<double>>* rv,
                                vector<vector<double>>* pb,
                                int numTimeSteps, int numPaths,
                                vector<double> timeNodes,
                                double c1, double c2);

int main(){

    int numTimeSteps = 7;
    int numPaths = 2000000;

    vector<vector<double>> rv(numTimeSteps, vector<double>(numPaths));
    //Fill rv

    vector<double> timeNodes(numTimeSteps);
    //Fill timeNodes

    vector<vector<double>> pb(numTimeSteps, vector<double>(numPaths, 0));

    computeOnGPU(&rv, &pb, numTimeSteps, numPaths, timeNodes, 0.2, 0.123);

}

3 个答案:

答案 0 :(得分:3)

正如其他人所说的那样,共享内存版本根本不会改变全局内存访问模式,并且在线程之间的内核中实际上没有数据重用。因此,合并问题没有得到解决,您正在实际做的就是添加共享内存访问和几个同步点作为开销。

但是看一下内核实际上做了一分钟。内核工作在双精度,在消费卡上很慢,并且在计算循环中具有相当合理的操作数,这很好。如果没有访问编译器,我猜大约一半的时间是浮点计算在exp调用和sqrt调用的一半。这可能不应该是消费者GPU上的内存绑定内核。但大约一半的双精度操作只是每个线程计算相同的 sqrt(dt)值。这是一个巨大的周期浪费。为什么不让内核在“非维度化的”sqrt(dt)域中迭代。这意味着您预先计算主机上的(最多)2000 sqrt(dt)值,并将它们存储在常量内存中。然后可以将内核循环写成类似于:

的东西
double pb0 = pb[p];
for(int i = 0; i < c_numTimeSteps; i++)
{
    double sdt = c_stimeNodes[i]; // sqrt(dt)
    double mdt = c_c2 * sdt * sdt;
    sdt *= c_c1;

    double pb1 = pb0 *  exp(mdt + sdt * rv[p]);

    p += c_numPaths;
    pb[p] = pb1;
    pb0 = pb1;
}

[免责声明:凌晨5点在拉普兰中间用ipad写的。使用风险自负]

这样做会用一个乘法替换一个sqrt,这大大减少了操作。 请注意,我还可以将索引计算简化为每个循环的单个整数加法。编译器很聪明,但你可以像你想的那样轻松或艰难地完成它的工作。我怀疑像上面这样的循环会比你现在的循环快得多。

答案 1 :(得分:2)

在我的Tesla M2090上分析您的代码后,我认为我们应该重新排列这些答案提供的所有这些建议。

  1. 尝试减少记忆时间。在memcopy上花费了97%的时间,包括H2D和D2H。由于您使用可分页的memcpy,速度为2.5G / s~3G / s。您可以使用pinned mem cpy加倍速度。可以应用零拷贝和其他Mem optimization技术来进一步提高记忆速度。

  2. 将sqrt()移出内核。您可以在CPU上执行7次sqrt()而不是在GPU上执行7次2,000,000次。但是,由于你的内核很小(占computePathGpu()总时间的3%),这不会产生太大影响。

  3. 减少全局内存访问。在您的代码中,您只需阅读rv一次,阅读pb一次并写一次pb。但是,在调用kenel之前,pb的第一行只包含有用的数据。因此,使用寄存器可以消除整个pb的读取。解决方案在代码中提供。

  4. 关于非合并内存访问,您可以找到讨论here。您的案例属于“顺序但未对齐的访问模式”。使用cudaMallocPitch()的解决方案如下所述,并在以下代码中提供。

  5. 注意:您提到您的DRAM利用率较低(约10%),但我的设备上的分析是可以的(55.8%)。也许这是我的设备有点旧(M2090 CC2.0)

    Profiling Result

    #include <vector>
    
    using namespace std;
    
    #define BLOCK_SIZE  64
    #define BLOCK_SIZE_OPT  256
    
    __constant__ double c_c1;
    __constant__ double c_c2;
    __constant__ int c_numTimeSteps;
    __constant__ int c_numPaths;
    __constant__ double c_timeNodes[2000];
    
    __global__ void kernelGlobalMem(double *rv, double *pb)
    {
        int p = blockDim.x * blockIdx.x + threadIdx.x;
    
        for (int i = 0; i < c_numTimeSteps; i++)
        {
            double dt = c_timeNodes[i];
            double sdt = sqrt(dt) * c_c1;
            double mdt = c_c2 * dt;
    
            pb[(i + 1) * c_numPaths + p] =
                    pb[i * c_numPaths + p] *
                            exp(mdt + sdt * rv[i * c_numPaths + p]);
    
        }
    
    }
    
    __global__ void kernelGlobalMemOpt(double *rv, double *pb, const size_t ld_rv, const size_t ld_pb)
    {
        int p = blockDim.x * blockIdx.x + threadIdx.x;
    
        double pb0 = pb[p];
        for (int i = 0; i < c_numTimeSteps; i++)
        {
            double dt = c_timeNodes[i];
            double sdt = dt * c_c1;
            double mdt = c_c2 * dt * dt;
    
            pb0 *= exp(mdt + sdt * rv[i * ld_rv + p]);
            pb[(i + 1) * ld_pb + p] = pb0;
        }
    }
    
    void computePathGpu(vector<vector<double> >* rv,
            vector<vector<double> >* pb,
            int numTimeSteps, int numPaths,
            vector<double> timeNodes,
            double c1, double c2)
    {
    
        cudaMemcpyToSymbol(c_c1, &c1, sizeof(double));
        cudaMemcpyToSymbol(c_c2, &c2, sizeof(double));
        cudaMemcpyToSymbol(c_numTimeSteps, &numTimeSteps, sizeof(int));
        cudaMemcpyToSymbol(c_numPaths, &numPaths, sizeof(int));
        cudaMemcpyToSymbol(c_timeNodes, &(timeNodes[0]), sizeof(double) * numTimeSteps);
    
        double *d_rv;
        double *d_pb;
    
        cudaMalloc((void**) &d_rv, sizeof(double) * numTimeSteps * numPaths);
        cudaMalloc((void**) &d_pb, sizeof(double) * (numTimeSteps + 1) * numPaths);
    
        vector<vector<double> >::iterator itRV;
        vector<vector<double> >::iterator itPB;
    
        double *dst = d_rv;
        for (itRV = rv->begin(); itRV != rv->end(); ++itRV)
        {
            double *src = &((*itRV)[0]);
            size_t s = itRV->size();
            cudaMemcpy(dst, src, sizeof(double) * s, cudaMemcpyHostToDevice);
            dst += s;
        }
    
        cudaMemcpy(d_pb, &((*(pb->begin()))[0]),
                sizeof(double) * (pb->begin())->size(), cudaMemcpyHostToDevice);
    
        dim3 block(BLOCK_SIZE);
        dim3 grid((numPaths + BLOCK_SIZE - 1) / BLOCK_SIZE);
    
        kernelGlobalMem<<<grid, block>>>(d_rv, d_pb);
        //kernelSharedMem<<<grid, block>>>(d_rv, d_pb);
        cudaDeviceSynchronize();
    
        dst = d_pb;
        for (itPB = ++(pb->begin()); itPB != pb->end(); ++itPB)
        {
            double *src = &((*itPB)[0]);
            size_t s = itPB->size();
            dst += s;
            cudaMemcpy(src, dst, sizeof(double) * s, cudaMemcpyDeviceToHost);
        }
    
        cudaFree(d_pb);
        cudaFree(d_rv);
    
    }
    
    void computePathGpuOpt(vector<vector<double> >* rv,
            vector<vector<double> >* pb,
            int numTimeSteps, int numPaths,
            vector<double> timeNodes,
            double c1, double c2)
    {
        for(int i=0;i<timeNodes.size();i++)
        {
            timeNodes[i]=sqrt(timeNodes[i]);
        }
    
        cudaMemcpyToSymbol(c_c1, &c1, sizeof(double));
        cudaMemcpyToSymbol(c_c2, &c2, sizeof(double));
        cudaMemcpyToSymbol(c_numTimeSteps, &numTimeSteps, sizeof(int));
        cudaMemcpyToSymbol(c_numPaths, &numPaths, sizeof(int));
        cudaMemcpyToSymbol(c_timeNodes, &(timeNodes[0]), sizeof(double) * numTimeSteps);
    
        double *d_rv;
        double *d_pb;
        size_t ld_rv, ld_pb;
    
        cudaMallocPitch((void **) &d_rv, &ld_rv, sizeof(double) * numPaths, numTimeSteps);
        cudaMallocPitch((void **) &d_pb, &ld_pb, sizeof(double) * numPaths, numTimeSteps + 1);
        ld_rv /= sizeof(double);
        ld_pb /= sizeof(double);
    
    //  cudaMalloc((void**) &d_rv, sizeof(double) * numTimeSteps * numPaths);
    //  cudaMalloc((void**) &d_pb, sizeof(double) * (numTimeSteps + 1) * numPaths);
    
        vector<vector<double> >::iterator itRV;
        vector<vector<double> >::iterator itPB;
    
        double *dst = d_rv;
        for (itRV = rv->begin(); itRV != rv->end(); ++itRV)
        {
            double *src = &((*itRV)[0]);
            size_t s = itRV->size();
            cudaMemcpy(dst, src, sizeof(double) * s, cudaMemcpyHostToDevice);
            dst += ld_rv;
        }
    
        cudaMemcpy(d_pb, &((*(pb->begin()))[0]),
                sizeof(double) * (pb->begin())->size(), cudaMemcpyHostToDevice);
    
        dim3 block(BLOCK_SIZE_OPT);
        dim3 grid((numPaths + BLOCK_SIZE_OPT - 1) / BLOCK_SIZE_OPT);
    
        kernelGlobalMemOpt<<<grid, block>>>(d_rv, d_pb, ld_rv, ld_pb);
        //kernelSharedMem<<<grid, block>>>(d_rv, d_pb);
        cudaDeviceSynchronize();
    
        dst = d_pb;
        for (itPB = ++(pb->begin()); itPB != pb->end(); ++itPB)
        {
            double *src = &((*itPB)[0]);
            size_t s = itPB->size();
            dst += ld_pb;
            cudaMemcpy(src, dst, sizeof(double) * s, cudaMemcpyDeviceToHost);
        }
    
        cudaFree(d_pb);
        cudaFree(d_rv);
    
    }
    
    int main()
    {
    
        int numTimeSteps = 7;
        int numPaths = 2000000;
    
        vector<vector<double> > rv(numTimeSteps, vector<double>(numPaths));
        vector<double> timeNodes(numTimeSteps);
        vector<vector<double> > pb(numTimeSteps, vector<double>(numPaths, 0));
        vector<vector<double> > pbOpt(numTimeSteps, vector<double>(numPaths, 0));
        computePathGpu(&rv, &pb, numTimeSteps, numPaths, timeNodes, 0.2, 0.123);
        computePathGpuOpt(&rv, &pbOpt, numTimeSteps, numPaths, timeNodes, 0.2, 0.123);
    }
    

    每个cuda线程计算所有时间步长的一条路径。根据您的GlobalMem代码,您不会在路径之间共享任何数据。因此不需要共享内存。

    对于nvprof检测到的非合并访问问题,这是因为您的数据pb和rv没有很好地对齐。 pb和rv可以看作是大小[时间步长x #paths]的矩阵。由于#path不是高速缓存行的倍数,从第二行开始,即时间步,所有全局内存访问都是非合并的。如果您的CUDA设备是旧的,它将导致50%的内存重播。较新的设备不会受到这种非合并访问的影响。

    解决方案很简单。您只需在行的每一端添加填充字节,这样每行都可以从合并的DRAM地址开始。这可以通过cudaMallocPitch()

    自动完成

    还有另一个问题。在你的代码中,你只需读取rv一次,读取pb一次并写入pb一次。 但是,在调用kenel之前,您的pb不包含任何有用的数据。因此,通过使用寄存器可以消除pb的读取,除了解决非合并访问问题之外,您还可以获得额外50%的速度。

答案 2 :(得分:0)

kernelGlobalMem 3 * c_numTimeSteps rvpbkernelSharedMem进行读/写。

3 * c_numTimeSteps + c_numTimeSteps / SHMEM_ROWS rv pbkernelSharedMemkernelGlobalMem进行读/写。

kernelSharedMem更复杂,内存模式看起来很相似。

{{1}}肯定比{{1}}好。