f = Vector(x^2*y^2 / y^2 / x^2*z^2)
我会像那样实现它:
double myFunc(int function_index)
{
switch(function_index)
{
case 1:
return PNT[0]*PNT[0]*PNT[1]*PNT[1];
case 2:
return PNT[1]*PNT[1];
case 3:
return PNT[2]*PNT[2]*PNT[1]*PNT[1];
}
}
而PNT
全局定义如下:double PNT[ NUM_COORDINATES ]
。现在我想为每个坐标实现每个函数的导数,从而生成导数矩阵(列=坐标;行=单个函数)。我已经编写了我的内核,它已经工作到目前为止,它调用了myFunc()。
问题是:为了计算关于坐标j的数学子函数i的导数,我将在顺序模式(例如在CPU上)使用以下代码(而这通常被简化,因为通常你会减少h,直到达到你的衍生物的某个精度):
f0 = myFunc(i);
PNT[ j ] += h;
derivative = (myFunc(j)-f0)/h;
PNT[ j ] -= h;
现在我想在GPU上并行执行此操作,问题出现了:如何处理PNT?由于我必须用h增加某些坐标,计算该值并再次减少它,会出现问题:如何在不“干扰”其他线程的情况下执行此操作?我无法修改PNT
,因为其他线程需要“原始”点来修改自己的坐标。
我的第二个想法是为每个线程保存一个修改点但是我很快就放弃了这个想法,因为当并行使用几千个线程时,这是一个非常糟糕的并且可能很慢(可能因为内存而根本无法实现)限制)的想法。
'最终'解决方案
所以我现在如何做到以下几点,它通过预处理器宏在coord_index
标识的坐标上添加运行时的值'add'(不存储它)。
#define X(n) ((coordinate_index == n) ? (PNT[n]+add) : PNT[n])
__device__ double myFunc(int function_index, int coordinate_index, double add)
{
//*// Example: f[i] = x[i]^3
return (X(function_index)*X(function_index)*X(function_index));
// */
}
效果非常好而且快速。当使用具有10000个函数和10000个坐标的导数矩阵时,它只需要0.5个参数。 PNT
全局定义或定义为常量内存,如__constant__ double PNT[ NUM_COORDINATES ];
,具体取决于预处理器变量USE_CONST
。
行return (X(function_index)*X(function_index)*X(function_index));
只是一个示例,其中每个子函数看起来都是相同的方案,数学上说:
f = Vector(x0^3 / x1^3 / ... / xN^3)
现在出现了大问题:
myFunc
是一个数学函数,用户应该能够按照自己喜欢的方式实现。例如。他还可以实现以下数学函数:
f = Vector(x0^2*x1^2*...*xN^2 / x0^2*x1^2*...*xN^2 / ... / x0^2*x1^2*...*xN^2)
因此,每个功能看起来都一样。作为程序员,您应该只编写一次代码,而不是依赖于实现的数学函数。因此,当在C ++中实现上述函数时,它看起来如下所示:
__device__ double myFunc(int function_index, int coordinate_index, double add)
{
double ret = 1.0;
for(int i = 0; i < NUM_COORDINATES; i++)
ret *= X(i)*X(i);
return ret;
}
现在内存访问非常“奇怪”而且对性能问题不利,因为每个线程需要访问PNT
的每个元素两次。当然,在每个函数看起来相同的情况下,我可以重写围绕调用myFunc
的完整算法,但正如我已经说过的那样:我不想根据用户实现的函数进行编码myFunc
...
有人能想出如何解决这个问题吗? 谢谢!
答案 0 :(得分:1)
重新回到开头,从一张干净的纸张开始,看起来你想要做两件事
虽然该函数具有标量值和任意性,但事实上,这个函数可以采用两种清晰的形式:
你似乎已经开始使用第一种类型的函数并且已经将代码放在一起来处理函数和近似导数的计算,现在正在努力解决如何使用相同代码处理第二种情况的问题。
如果这是对问题的合理总结,那么请在评论中注明,我将继续使用一些代码示例和概念进行扩展。如果不是,我会在几天后将其删除。
在评论中,我一直试图建议将第一类功能与第二类功能混淆不是一个好方法。并行执行的正确性要求以及在GPU上提取并行性和性能的最佳方法是非常不同的。通过在具有不同使用模型的两个不同代码框架中分别处理这两种类型的函数,可以更好地服务。当需要实现给定的数学表达式时,“用户”应该对该表达式是否类似于第一类函数或第二类函数的模型进行基本分类。分类行为是驱动代码中算法选择的动力。这种类型的“按算法分类”在设计良好的库中几乎是通用的 - 您可以在Boost和STL等C ++模板库中找到它,您可以在BLAS等传统的Fortran代码中找到它。