JavaScript中的GPU并行性变慢了

4

这是一个比较具体的问题。我最近尝试了 gpu.js 库。该库使用WebGL并行计算来加速计算过程。我进行了一次快速测试:

var gpu = new GPU();

function product(v, u) {
  return gpu.createKernel(function(X, Y) {
      return X[this.thread.x] * Y[this.thread.x];
  }).dimensions([v.length])(v, u);
}


var before = new Date().getTime();
console.log(product(numeric.random([100000]), numeric.random([100000])).length);
console.log('Parallel Time: ', (new Date().getTime()) - before);

before = new Date().getTime();
v = numeric.random([100000])
u = numeric.random([100000])
for(var i = 0; i < v.length; i++){
  v[i] = v[i] * u[i];
}
console.log(v.length);
console.log('Procedural Time: ', (new Date().getTime()) - before);

并获得了以下输出:

script.js:11 100000 
script.js:12 Parallel Time:  340 
script.js:20 100000 
script.js:21 Procedural Time:  15

并行时间比顺序时间慢了一个数量级,这是有什么原因吗?我在几台不同的GPU上尝试过,也尝试过一些类似的操作。我是做错了什么还是库的问题?有什么方法可以改进吗?


如果你在 http://gpu.rocks/ 上运行基准测试,你会得到显著的加速吗? - Jaromanda X
是的,我最初就这么做了,速度提高了5.72倍。我不确定我做错了什么。 - user5505266
3个回答

3
处理GPU时,必须注意开销。调用 gpu.createKernel 可能非常昂贵,因为它必须解析JavaScript代码,创建适当的GLSL代码,并将其发送到WebGL进行编译和链接。至少您需要调用该命令一次,并将结果存储在全局变量中,以便在每次调用product时重复使用。
还值得注意的是,移动数据到GPU和从GPU返回所需的工作量不为零,因此在进行更复杂的计算时会看到更多收益。

谢谢你的回答,我正在输入我的答案时看到了它。我认为这是一个开销问题,但我仍然通过每次调用来使其工作。如果我真的用它做些什么,我一定会遵循你的建议。谢谢! - user5505266
一个更可比较的基准测试好地方是将 .dimensions([v.length])(v, u) 分解成 var kernel = ....dimensions([v.length]); kernel(v, u),并在调用中间的 kernel(v, u) 之前启动计时器。 - Patrick Roberts

1
我查看了他们基准测试的源代码,发现只有在连续运行大量操作时才能获得加速效果。我认为这是一个开销问题。我创建了以下超级简单的基准测试,将gpu.js与numeric.js进行比较。如果有人感兴趣,可以看看:
var gpu = new GPU();

var size = 512;
var scale = 10;
var iterations = 100;

// Scaling up the matricies decreases the effect of precision errors
A = numeric.mul(numeric.random([size, size]), scale)
B = numeric.mul(numeric.random([size, size]), scale)

// I know eval is dangerous but I couldn't get the size in any other way
function multGen(size) {
  return eval("(function(A, B) { var sum = 0; for (var i=0; i<"+ size +"; i++) {sum += A[this.thread.y][i] * B[i][this.thread.x];} return sum;})")
}

var mat_mult = gpu.createKernel(multGen(size)).dimensions([size, size]);

var before = new Date().getTime();
var parallel = mat_mult(A, B);

// Need to do many computations to get the advantages of the GPU
for(var i = 0; i < iterations; i++) {
  parallel = mat_mult(A, B);
}
var parTime = (new Date().getTime()) - before;
console.log('Parallel Time: ', parTime);

before = new Date().getTime();
var procedural = numeric.dot(A, B);

// Need to do many computations to get the advantages of the GPU
for(var i = 0; i < iterations; i++) {
  procedural = numeric.dot(A, B);
}
var procTime = (new Date().getTime()) - before;
console.log('Procedural Time: ', procTime);

console.log((procTime / parTime) + ' times faster');

// This is for RMSD nornalization, flattening and doing min and max that way exceeded the call stack
var max = Math.max(Math.max(...A.map((function(row) {return Math.max(...row);}))), Math.max(...B.map((function(row) {return Math.max(...row);}))))

var min = Math.min(Math.min(...A.map((function(row) {return Math.min(...row);}))), Math.min(...B.map((function(row) {return Math.min(...row);}))))

// The matricies will be different due to precision issues so the Normalized RMDS can give you an idea of the difference
var nrmsd = Math.sqrt(numeric.sum(numeric.pow(numeric.sub(parallel, procedural), 2)) / size) / (max - min);

console.log('Normalized RMSD: ', nrmsd);

这给了我以下输出:
scriptfour.js:26 Parallel Time:  20490
scriptfour.js:36 Procedural Time:  28736
scriptfour.js:38 1.402440214738897 times faster
scriptfour.js:48 Normalized RMSD:  0.009671934749138042

这些结果相当不错。评估使并行处理变慢了,但它仍然比单线程快。我认为这样的设置不适合生产环境,但在这里仍然有效。

您可以将 size 指定为常量:https://github.com/gpujs/gpu.js#dynamic-sized-via-constants - Patrick Roberts

0

使用:

t0 = performance.now();
yourFunctionCall();
t1 = performance.now();
console.log("Function yourFunctionCall took " + (t1 - t0) + " ms.");

不确定那是否是问题的核心,但我也遇到了日期相关的问题。


不是核心问题,但感谢您的提示。我已经用日期解决了这个问题,但如果今后遇到类似情况,我会优化性能。谢谢! - user5505266

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接