我在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);
}
答案 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上分析您的代码后,我认为我们应该重新排列这些答案提供的所有这些建议。
尝试减少记忆时间。在memcopy上花费了97%的时间,包括H2D和D2H。由于您使用可分页的memcpy,速度为2.5G / s~3G / s。您可以使用pinned mem cpy加倍速度。可以应用零拷贝和其他Mem optimization技术来进一步提高记忆速度。
将sqrt()移出内核。您可以在CPU上执行7次sqrt()而不是在GPU上执行7次2,000,000次。但是,由于你的内核很小(占computePathGpu()
总时间的3%),这不会产生太大影响。
减少全局内存访问。在您的代码中,您只需阅读rv
一次,阅读pb
一次并写一次pb
。但是,在调用kenel之前,pb
的第一行只包含有用的数据。因此,使用寄存器可以消除整个pb
的读取。解决方案在代码中提供。
关于非合并内存访问,您可以找到讨论here。您的案例属于“顺序但未对齐的访问模式”。使用cudaMallocPitch()的解决方案如下所述,并在以下代码中提供。
注意:您提到您的DRAM利用率较低(约10%),但我的设备上的分析是可以的(55.8%)。也许这是我的设备有点旧(M2090 CC2.0)
#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
rv
在pb
和kernelSharedMem
进行读/写。
3 * c_numTimeSteps + c_numTimeSteps / SHMEM_ROWS
rv
pb
在kernelSharedMem
和kernelGlobalMem
进行读/写。
kernelSharedMem
更复杂,内存模式看起来很相似。
{{1}}肯定比{{1}}好。