为什么在 Matlab 脚本中使用 gpuArray 时 a*b*a 比 (a'*(a*b)')' 花费的时间更长?

why does a*b*a take longer than (a'*(a*b)')' when using gpuArray in Matlab scripts?

下面的代码以两种不同的方式对 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 倍。这是 Nvidia K20 上 R2018a 中上述脚本的输出(我尝试过具有类似行为的不同版本和不同 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 函数中而不是在脚本中。

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

编辑:他们使用MAGMA,这是专栏专业。我的回答不成立,但我会把它留在这里一段时间,以防它能帮助破解这种奇怪的行为。

下面的答案是错误的

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


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

重要信息

  • MATLAB 是列专业,CUDA 是行专业。
  • 没有二维矩阵,只有带有 2 个索引的一维矩阵

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

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

  1. 在 CUDA 中编写您自己的列主代数库
  2. 从 MATLAB 中取出每个 input/output 并转置它(即从列优先转换为行优先)

他们已经完成了第2点,假设有一个用于MATLAB并行处理工具箱的智能JIT编译器(合理假设),对于第二种情况,需要ab,转置他们,做数学运算,并在 gather.

时转置输出

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

确实这里有一件奇怪的事情:把代码变成一个函数突然变慢了。我最好的猜测是,由于 JIT 在不同情况下的行为不同,它不会捕获所有这些 transpose 内部的东西,只是执行所有操作,从而失去速度。


有趣的观察:在我的 PC 中 CPU 与 GPU 花费的时间相同 a*b*a

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

在您的上下文中,它是最后一个乘法。为了演示我将 a 替换为重复的 c,并乘以它两次,一次作为稀疏矩阵,一次作为完整矩阵。

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涉及两个稀疏矩阵的乘法(aa 再次)并且没有得到很好的处理,而使用 transpose() 方法将过程分为两个阶段乘法,在none中有两个稀疏矩阵。

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

瑞兰的回复:

感谢您在此问题上的耐心等待。我咨询了 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 post.

中 '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