我想写一个轻量级的PIC(粒子在单元格)程序。 “轻量级”我的意思是它不需要扩展:只是假设所有数据都可以适合单个GPU设备的内存和主机系统的内存。但是我希望它尽可能快。
问题是,PIC的典型结构是两个阶段的相互作用:场解算器和粒子推动器。工作流程如下: 初始化系统 - >推动粒子 - >求解字段 - >推动粒子 - >解决领域...... - >输出
下一个推动粒子或求解场必须等到前一个求解场或推动粒子完成。可能需要数百万次迭代才能获得最终输出。
作为测试,省略场解算器,粒子推动器可以写成:
__device__
void push(Particle &par) {
// some routines to move a particle. same excecutiong time for every particle.
}
并使用像这样的kernel_1来超越它:
__global__
void kernel_1(int n, Particle* parlist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i < n) {
push(parlist[i]);
}
}
在主循环中
for (int i=0;i<M;i++) {
kernel_1<<<(n+255)/256, 256>>>(n, parlist);
}
M是所需的迭代次数。但是,性能非常慢:在我的八核Intel E5-2640 v3和Nvidia Quadro m4000系统上,CUDA使用openmp提供与纯CPU版本相似的性能。对于粒子数10,000,000和M = 1000,需要大约10秒。
但是,如果我将循环移动到内核中:
void kernel_2(int n, Particle* parlist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i < n) {
for (int i=0;i<M;i++) {
push(parlist[i]);
}
}
}
和
kernel_2<<<(n+255)/256, 256>>>(n, parlist);
对于相同的M = 1000,它只需要100ms,这是10000%的加速。我已经验证了两种情况下结果相同且正确。也许M次运行内核的调用成本太高了。
将循环移入内核的性能提升是如此令人难以置信,但却是如此。对于第一种情况,可以很容易地添加字段求解器:只需编写一个新内核并在主循环中按顺序执行两个内核。但是表现应该是医学上的。
我发现很难将字段求解程序添加到第二种情况中:在没有多次调用内核的情况下,块之间似乎没有同步机制,但是场解算器必须等到所有粒子都被推送,必须分配到不同的块(因为粒子的数量非常高)。
那么可以在一个内核中实现两阶段迭代吗?性能提升太多而不容忽视。
修改 我发现性能差异非常混乱:100ms和10s的差异只是一行代码甚至是循环序列。我已经将push()修改为更复杂(2d Boris推动器):
class Particle
{
public:
float x, y; //m
float vx, vy; //m/s
float m; //kg
float q; //ee
};
__device__
void run(Particle& par, float B)
{
float t, s, vpx, vpy;
t = (par.q*ee*B/par.m)*dt/2;
s = 2*t/(1+t*t);
vpx = par.vx+t*par.vy;
vpy = par.vy-t*par.vx;
par.vx += s*vpy;
par.vy -= s*vpx;
par.x += par.vx*dt;
par.y += par.vy*dt;
}
我为Particle创建了1个n元素数组,为B创建了1个n元素浮点数组。它们在主机和cudaMemcpy上创建到设备。然后我检查了以下三个内核的性能:
__global__
void kernel_A(int n, int m, Particle* parlist, float* Blist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
int j;
if (i<n) {
for (j=0;j<m;j++) {
run(parlist[i], Blist[i]);
}
}
}
__global__
void kernel_B(int n, int m, Particle* parlist, float* Blist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
int j;
float B;
if (i<n) {
B = Blist[i];
for (j=0;j<m;j++) {
run(parlist[i], B);
}
}
}
__global__
void kernel_C(int n, int m, Particle* parlist, float* Blist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
int j;
float B;
if (i<n) {
B = Blist[i];
for (j=0;j<m;j++) {
run(parlist[i], B);
__syncthreads();
}
}
}
__global__
void kernel_D(int n, int m, Particle* parlist, float* Blist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
int j;
float B;
if (i<n) {
B = Blist[i];
}
for (j=0;j<m;j++) {
if (i<n) {
run(parlist[i], B);
}
}
}
__global__
void kernel_E(int n, int m, Particle* parlist, float* Blist)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
int j;
float B;
if (i<n) {
for (j=0;j<m;j++) {
run(parlist[i], Blist[i]);
__syncthreads();
}
}
}
运行时间完全不同。对于n = 10,000,000且m = 1000:
三个内核的结果完全相同且正确(根据CPU版本进行检查)。
我从官方的CUDA编程指南中了解到分支是昂贵的,因此kernel_C应该比kernel_B慢,但我怀疑差异是两个数量级。我不明白的是为什么kernel_B比kernel_A好得多。当kernel_A执行时,Kernel_B不必访问Blist 1000次,但是他们都需要访问parlist 1000次吗?为什么访问Blist这么慢?
Kernel_A,kernel_D和kernel_E有类似的性能,这让我更加困惑:所以与kernel_B相比的额外时间用于访问Blist或同步?
我想在PIC程序中实现kernel_B的性能。
答案 0 :(得分:1)
不,无法在块之间进行同步。通常,内核调用带来的开销并不重要。我可以想象,你的内核不够大,无法在很大程度上利用你的设备。如果你想检查这个,你可以使用nvprof来分析你的程序并找出瓶颈。
实现快速PIC代码并不容易。您是否考虑使用PIConGPU等库?您可以在以下链接中找到它:https://github.com/ComputationalRadiationPhysics/picongpu