为什么a * b * a需要的时间超过(a' *(a * b)')'在Matlab脚本中使用gpuArray时?

时间:2018-05-01 03:36:29

标签: matlab matrix gpu sparse-matrix linear-algebra

下面的代码以两种不同的方式对gpuArrays ab执行相同的操作。第一部分计算(a'*(a*b)')',第二部分计算a*b*a。然后验证结果是相同的。

%function test
clear
rng('default');rng(1);
a=sprand(3000,3000,0.1);
b=rand(3000,3000);
a=gpuArray(a);
b=gpuArray(b);
tic;
c1=gather(transpose(transpose(a)*transpose(a*b)));
disp(['time for (a''*(a*b)'')'': ' , num2str(toc),'s'])

clearvars -except c1

rng('default');
rng(1)
a=sprand(3000,3000,0.1);
b=rand(3000,3000);
a=gpuArray(a);
b=gpuArray(b);
tic;
c2=gather(a*b*a);
disp(['time for a*b*a: ' , num2str(toc),'s'])

disp(['error = ',num2str(max(max(abs(c1-c2))))])

%end

但是,计算(a'*(a*b)')'大约比计算a*b*a快4倍。以下是R2018a上Nvidia K20上面脚本的输出(我尝试过不同的版本和不同的GPU,具有相似的行为)。

>> test
time for (a'*(a*b)')': 0.43234s
time for a*b*a: 1.7175s
error = 2.0009e-11

更奇怪的是,如果上述脚本的第一行和最后一行被取消注释(将其转换为函数),则两者都需要更长的时间(~1.7s而不是~0.4s)。以下是此案例的输出:

>> test
time for (a'*(a*b)')': 1.717s
time for a*b*a: 1.7153s
error = 1.0914e-11

我想知道导致此行为的原因,以及如何在较短的时间内执行a*b*a(a'*(a*b)')'或两者(即~0.4s而不是~1.7s) )在matlab函数内部而不是在脚本内部。

3 个答案:

答案 0 :(得分:6)

GPU上两个稀疏矩阵的乘法似乎存在问题。稀疏全矩阵的时间比稀疏的稀疏快1000倍。一个简单的例子:

str={'sparse*sparse','sparse*full'};
for ii=1:2
    rng(1);
    a=sprand(3000,3000,0.1);
    b=sprand(3000,3000,0.1);
    if ii==2
        b=full(b);
    end
    a=gpuArray(a);
    b=gpuArray(b);
    tic
    c=a*b;
    disp(['time for ',str{ii},': ' , num2str(toc),'s'])
end

在你的上下文中,它是最后一个乘法。演示我用重复的 c 替换 a ,然后乘以它两次,一次是稀疏的,一次是全矩阵。

str={'a*b*a','a*b*full(a)'};
for ii=1:2
    %rng('default');
    rng(1)
    a=sprand(3000,3000,0.1);
    b=rand(3000,3000);
    rng(1)
    c=sprand(3000,3000,0.1);
    if ii==2
        c=full(c);
    end
    a=gpuArray(a);
    b=gpuArray(b);
    c=gpuArray(c);
    tic;
    c1{ii}=a*b*c;
    disp(['time for ',str{ii},': ' , num2str(toc),'s'])
end
disp(['error = ',num2str(max(max(abs(c1{1}-c1{2}))))])

我可能错了,但我的结论是 a * b * a 涉及两个稀疏矩阵的乘法运算( a再次 a 并且没有得到很好的处理,而使用transpose()方法将过程分为两阶段乘法,其中没有一个有两个稀疏矩阵。

答案 1 :(得分:3)

编辑2 我可能是对的,请参阅this other answer

编辑:他们使用MAGMA,这是专栏。我的回答并不成立,但是如果它可以帮助解决这种奇怪的行为,我会暂时搁置一段时间。

以下答案错误

这是我的猜测,我不能100%告诉你不知道MATLAB的代码。

假设: MATLAB并行计算代码使用的是CUDA库,而不是它们自己的库。

重要信息

  • MATLAB是专栏,CUDA是行专业。
  • 没有2D矩阵这样的东西,只有2个索引的1D矩阵

为什么这很重要?好吧,因为CUDA是高度优化的代码,它使用内存结构来最大化每个内核的缓存命中率(GPU上最慢的操作是读取内存)。这意味着标准的CUDA矩阵乘法代码将利用内存读取的顺序来确保它们是相邻的。但是,行主要中的相邻内存不在列主要内容中。

因此,作为编写软件的人有两种解决方案

  1. 在CUDA中编写自己的列主代数库
  2. 从MATLAB获取每个输入/输出并转置它(即从列主要转换为行主要)
  3. 他们已经完成了第2点,并且假设有一个用于MATLAB并行处理工具箱的智能JIT编译器(合理的假设),对于第二种情况,需要ab转换它们,做数学,并在gather时转置输出。

    然而,在第一种情况下,您已经不需要转置输出,因为它在内部已经转置并且JIT捕获了它,因此它不是调用gather(transpose( XX ))而是跳过输出转置的一侧。与transpose(a*b)相同。请注意transpose(a*b)=transpose(b)*transpose(a),因此突然不需要转置(它们都在内部被跳过)。换位是一项昂贵的操作。

    确实有一个奇怪的事情:使代码成为一个函数突然变得缓慢。我最好的猜测是,因为JIT在不同的情况下表现不同,所以它并没有捕获所有这些transpose内容,无论如何都会进行所有操作,从而失去速度。

    有趣的观察:在我的电脑上a*b*a执行Where的CPU需要与GPU相同的时间。

答案 2 :(得分:3)

我与Mathworks技术支持取得了联系,Rylan终于对这个问题有所了解。 (谢谢Rylan!)他的完整回应如下。函数vs脚本问题似乎与某些优化有关,matlab自动应用于函数(但不是脚本)不能按预期工作。

<> Rylan的回应:

感谢您对此问题的耐心等待。我已经咨询过MATLAB GPU计算开发人员,以便更好地理解这一点。

此问题是由MATLAB在遇到矩阵 - 矩阵乘法和转置等特定操作时进行的内部优化引起的。在执行MATLAB函数(或匿名函数)而不是脚本时,可以特别启用其中一些优化。

当您从脚本执行初始代码时,不会执行特定的矩阵转置优化,这会导致'res2'表达式比'res1'表达式更快:

  n = 2000;
  a=gpuArray(sprand(n,n,0.01)); 
  b=gpuArray(rand(n));

  tic;res1=a*b*a;wait(gpuDevice);toc                                         % Elapsed time is 0.884099 seconds.
  tic;res2=transpose(transpose(a)*transpose(a*b));wait(gpuDevice);toc        % Elapsed time is 0.068855 seconds.

但是当上面的代码放在MATLAB函数文件中时,会进行额外的矩阵转置时间优化,这会导致'res2'表达式通过不同的代码路径(和不同的CUDA库函数调用)与从脚本调用的同一行相比。因此,当从函数文件调用时,此优化会为'res2'行生成较慢的结果。

为避免在函数文件中出现此问题,需要以阻止MATLAB应用此优化的方式拆分转置和乘法运算。在'res2'语句中分隔每个子句似乎就足够了:

  tic;i1=transpose(a);i2=transpose(a*b);res3=transpose(i1*i2);wait(gpuDevice);toc      % Elapsed time is 0.066446 seconds.

在上面的行中,'res3'是从两个中间矩阵生成的:'i1'和'i2'。从脚本执行时,性能(在我的系统上)似乎与'res2'表达式相当;此外,'res3'表达式在从MATLAB函数文件执行时也表现出类似的性能。但是请注意,可以使用额外的存储器来存储初始阵列的转置副本。如果您在系统上看到不同的性能行为,请与我们联系,我可以进一步调查。

此外,'res3'操作在使用'gputimeit'功能测量时也表现出更快的性能。有关详细信息,请参阅附带的'testscript2.m'文件。我还附加了'test_v2.m',它是Stack Overflow帖子中'test.m'函数的修改。

感谢您向我报告此问题。对于因此问题造成的任何不便,我们深表歉意。我创建了一个内部错误报告,通知MATLAB开发人员这种行为。他们可能会在将来的MATLAB版本中为此提供修复。

由于您还有一个问题,即使用'gputimeit'与使用'tic'和'toc'来比较GPU代码的性能,我只想提供MATLAB GPU计算开发人员之前提到过的一个建议。 。通常在'tic'语句之前调用'wait(gpuDevice)'以确保前一行的GPU操作在下一行的测量中不重叠。例如,在以下行中:

  b=gpuArray(rand(n));
  tic; res1=a*b*a; wait(gpuDevice); toc  

如果在'tic'之前没有调用'wait(gpuDevice)',那么从前一行构造'b'数组所花费的时间可能会重叠并在执行时间内计算'res1'表达式。这将是首选:

  b=gpuArray(rand(n));
  wait(gpuDevice); tic; res1=a*b*a; wait(gpuDevice); toc  

除此之外,我没有看到你使用'tic'和'toc'函数的方式有任何具体问题。但请注意,通常建议使用'gputimeit'而不是直接使用'tic'和'toc'进行与GPU相关的分析。

我会暂时关闭此案例,但如果您对此有任何疑问,请与我们联系。

%testscript2.m
n = 2000;
a = gpuArray(sprand(n, n, 0.01)); 
b = gpuArray(rand(n)); 

gputimeit(@()transpose_mult_fun(a, b))
gputimeit(@()transpose_mult_fun_2(a, b))

function out = transpose_mult_fun(in1, in2)

i1 = transpose(in1);
i2 = transpose(in1*in2);

out = transpose(i1*i2);

end

function out = transpose_mult_fun_2(in1, in2)

out = transpose(transpose(in1)*transpose(in1*in2));

end

function test_v2

clear

%% transposed expression
n = 2000;
rng('default');rng(1);
a = sprand(n, n, 0.1);
b = rand(n, n);
a = gpuArray(a);
b = gpuArray(b);

tic;
c1 = gather(transpose( transpose(a) * transpose(a * b) ));

disp(['time for (a''*(a*b)'')'': ' , num2str(toc),'s'])

clearvars -except c1

%% non-transposed expression
rng('default');
rng(1)
n = 2000;
a = sprand(n, n, 0.1);
b = rand(n, n);
a = gpuArray(a);
b = gpuArray(b);

tic;
c2 = gather(a * b * a);

disp(['time for a*b*a: ' , num2str(toc),'s'])
disp(['error = ',num2str(max(max(abs(c1-c2))))])

%% sliced equivalent
rng('default');
rng(1)
n = 2000;
a = sprand(n, n, 0.1);
b = rand(n, n);
a = gpuArray(a);
b = gpuArray(b);

tic;
intermediate1 = transpose(a);
intermediate2 = transpose(a * b);
c3 = gather(transpose( intermediate1 * intermediate2 ));

disp(['time for split equivalent: ' , num2str(toc),'s'])
disp(['error = ',num2str(max(max(abs(c1-c3))))])

end