我有一个数据处理任务,可以通过以下方式进行样式化。我有data
(~1-10GB)和一个函数,它根据此summary
和一些(双)输入data
生成x
(~1MB)。对于summary
的〜1000个值,我需要获得此x
,这看起来像GPU的完美任务。重复一下,输入data
对于所有线程都是相同的,并且以线性方式读取,但每个线程必须生成自己的summary
。函数针对不同的x
独立执行。
然而,在CPU上使用x
的所有值进行粗体单线程循环只会使性能比K520差3倍。我确实理解这是内存密集型任务(线程必须访问并写入summary
的随机部分),但我仍然很难理解GPU如何失去它最初的1000x优势。我已经尝试使用data
内存将__constant__
提供给块中的提要(因为它是所有线程的相同输入),没有明显的改进。 nvprof报告的典型块运行时间为10-30秒。
我很感激任何有关适合此任务的优化的见解。
编辑:下面是一个复制问题的示例代码。它可以在g ++(报告运行时间为5秒)和nvcc(报告运行时间为7秒)下编译。分析结果如下
== 23844 ==分析结果:
时间(%)时间调用平均最小最大名称
98.86%4.68899s 1 4.68899s 4.68899s 4.68899s内核(观察*,int *,信息**)
1.09%51.480ms 4 12.870ms 1.9200us 50.426ms [CUDA memcpy HtoD]
0.06%2.6634ms 800 3.3290us 3.2950us 5.1200us [CUDA memcpy DtoD]
0.00%4.3200us 1 4.3200us 4.3200us 4.3200us [CUDA memcpy DtoH]
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <ctime>
#include <cstring>
#define MAX_OBS 1000000
#define MAX_BUCKETS 1000
using namespace std;
// Cross-arch defines
#ifndef __CUDACC__
#define GPU_FUNCTION
#define cudaSuccess 0
typedef int cudaError_t;
struct dim3
{
int x;
int y;
int z;
} blockIdx, threadIdx;
enum cudaMemcpyKind
{
cudaMemcpyHostToDevice = 0,
cudaMemcpyDeviceToHost = 1,
cudaMemcpyDeviceToDevice = 2
};
cudaError_t cudaMalloc(void ** Dst, size_t bytes)
{
return !(*Dst = malloc(bytes));
}
cudaError_t cudaMemcpy(void * Dst, const void * Src, size_t bytes, cudaMemcpyKind kind)
{
return !memcpy(Dst, Src, bytes);
}
#else
#define GPU_FUNCTION __global__
#endif
// Basic observation structure as stored on disk
struct Observation
{
double core[20];
};
struct Info
{
int left;
int right;
};
GPU_FUNCTION void Kernel(Observation * d_obs,
int * d_bucket,
Info ** d_summaries)
{
Info * summary = d_summaries[threadIdx.x * 40 + threadIdx.y];
for (int i = 0; i < MAX_OBS; i++)
{
if (d_obs[i].core[threadIdx.x] < (threadIdx.x + 1) * threadIdx.y)
summary[d_bucket[i]].left++;
else
summary[d_bucket[i]].right++;
}
}
int main()
{
srand((unsigned int)time(NULL));
// Generate dummy observations
Observation * obs = new Observation [MAX_OBS];
for (int i = 0; i < MAX_OBS; i++)
for (int j = 0; j < 20; j++)
obs[i].core[j] = (double)rand() / RAND_MAX;
// Attribute observations to one of the buckets
int * bucket = new int [MAX_OBS];
for (int i = 0; i < MAX_OBS; i++)
bucket[i] = rand() % MAX_BUCKETS;
Info summary[MAX_BUCKETS];
for (int i = 0; i < MAX_BUCKETS; i++)
summary[i].left = summary[i].right = 0;
time_t start;
time(&start);
// Init device objects
Observation * d_obs;
int * d_bucket;
Info * d_summary;
Info ** d_summaries;
cudaMalloc((void**)&d_obs, MAX_OBS * sizeof(Observation));
cudaMemcpy(d_obs, obs, MAX_OBS * sizeof(Observation), cudaMemcpyHostToDevice);
cudaMalloc((void**)&d_bucket, MAX_OBS * sizeof(int));
cudaMemcpy(d_bucket, bucket, MAX_OBS * sizeof(int), cudaMemcpyHostToDevice);
cudaMalloc((void**)&d_summary, MAX_BUCKETS * sizeof(Info));
cudaMemcpy(d_summary, summary, MAX_BUCKETS * sizeof(Info), cudaMemcpyHostToDevice);
Info ** tmp_summaries = new Info * [20 * 40];
for (int k = 0; k < 20 * 40; k++)
cudaMalloc((void**)&tmp_summaries[k], MAX_BUCKETS * sizeof(Info));
cudaMalloc((void**)&d_summaries, 20 * 40 * sizeof(Info*));
cudaMemcpy(d_summaries, tmp_summaries, 20 * 40 * sizeof(Info*), cudaMemcpyHostToDevice);
for (int k = 0; k < 20 * 40; k++)
cudaMemcpy(tmp_summaries[k], d_summary, MAX_BUCKETS * sizeof(Info), cudaMemcpyDeviceToDevice);
#ifdef __CUDACC__
Kernel<<<1, dim3(20, 40, 1)>>>(d_obs, d_bucket, d_summaries);
#else
for (int k = 0; k < 20 * 40; k++)
{
threadIdx.x = k / 40;
threadIdx.y = k % 40;
Kernel(d_obs, d_bucket, d_summaries);
}
#endif
cudaMemcpy(summary, d_summary, MAX_BUCKETS * sizeof(Info), cudaMemcpyDeviceToHost);
time_t end;
time(&end);
cout << "Finished calculations in " << difftime(end, start) << "s" << endl;
cin.get();
return 0;
}
编辑2:我尝试通过并行处理分散的内存访问来重新编写代码。简而言之,我的新内核看起来像这样
__global__ void Kernel(Observation * d_obs,
int * d_bucket,
double * values,
Info ** d_summaries)
{
Info * summary = d_summaries[blockIdx.x * 40 + blockIdx.y];
__shared__ Info working_summary[1024];
working_summary[threadIdx.x] = summary[threadIdx.x];
__syncthreads();
for (int i = 0; i < MAX_OBS; i++)
{
if (d_bucket[i] != threadIdx.x) continue;
if (d_obs[i].core[blockIdx.x] < values[blockIdx.y])
working_summary[threadIdx.x].left++;
else
working_summary[threadIdx.x].right++;
}
__syncthreads();
summary[threadIdx.x] = working_summary[threadIdx.x];
}
<<<dim(20, 40, 1), 1000>>>
需要18秒,<<<dim(20,40,10), 1000>>>
需要172秒---这比单个CPU线程更糟糕,并且线性增加了并行任务的数量。
答案 0 :(得分:2)
你正在使用的K520主板有两个GPU,每个都有8个流式多处理器,我相信,每个GPU的峰值带宽约为160 GB / s。使用上面的代码,你应该受到这个带宽的限制,并且应该考虑每个GPU至少获得100 GB / s(尽管我的目标是单个GPU启动)。也许你无法击中它,也许你会击败它,但它是一个很好的目标。
要做的第一件事是修复启动参数。这一行:
Kernel<<<1, dim3(20, 40, 1)>>>(d_obs, d_bucket, d_summaries);
表示您正在启动1个包含800个线程的CUDA块。这远远不够GPU的并行性。您需要至少与流式多处理器(即8)一样多的块,最好是更多(即100+)。这将为您带来巨大的性能提升。对于GPU而言,800路并行性还不够。
GPU对访问模式非常敏感。以下代码:
summary[d_bucket[i]].left++;
将一个分散的4字节写入摘要。分散的内存事务在GPU上是昂贵的,并且为了在内存绑定代码上的合理性能,应该避免它们。在这种情况下我们能做些什么呢?在我看来,解决方案是增加更多的并行性。而不是每个线程的摘要,每个块都有一个摘要。每个线程都可以处理范围0...MAX_OBS
的子集,并且可以递增应该位于shared memory
中的块范围的汇总数组。在内核的末尾,您可以将结果写回全局内存。令人高兴的是,这也解决了上面提到的缺乏并行性问题!
此时你应该找出一种衡量改善空间的方法。你会想知道你有多接近峰值带宽(我发现最好考虑你必须移动的数据,以及你实际移动的数据),如果你仍然明显偏离它,你想看看如果可能的话,还要进一步减少存储器访问和优化访问。