почему a * b * a занимает больше времени, чем (a '* (a * b)') 'при использовании gpuArray в сценариях Matlab? - PullRequest
0 голосов
/ 01 мая 2018

Приведенный ниже код выполняет ту же операцию для gpuArrays a и b двумя различными способами. Первая часть вычисляет (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)')' примерно в 4 раза быстрее, чем вычисления a*b*a. Вот вывод вышеприведенного скрипта в R2018a на Nvidia K20 (я пробовал разные версии и разные графические процессоры с похожим поведением).

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

Еще более странно, что если первая и последняя строки приведенного выше сценария не закомментированы (чтобы превратить его в функцию), то обе из них занимают больше времени (~ 1,7 с вместо 0,4 с). Ниже приведен вывод для этого случая:

>> 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,4 с, а не ~ 1,7 с) внутри функции matlab, а не чем внутри скрипта.

Ответы [ 3 ]

0 голосов
/ 07 мая 2018

Кажется, есть проблема с умножением двух разреженных матриц на 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 включает умножение двух разреженных матриц ( a и a снова) и не обрабатывается должным образом, а при использовании подхода transpose () процесс делится на двухступенчатое умножение, ни в одной из которых нет двух разреженных матриц.

0 голосов
/ 08 мая 2018

Я связался со службой технической поддержки Mathworks, и Рилан наконец-то пролил свет на эту проблему. (Спасибо, Рилан!) Его полный ответ ниже. Проблема функции и скрипта, по-видимому, связана с определенными оптимизациями, которые Matlab применяет автоматически к функциям (но не к скриптам), которые не работают должным образом

Ответ Райлана:

Спасибо за ваше терпение по этому вопросу. Я посоветовался с разработчиками вычислений на GPU MATLAB, чтобы лучше это понять.

Эта проблема вызвана внутренней оптимизацией, выполняемой 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', который является модификацией функции 'test.m' в вашем посте переполнения стека.

Спасибо, что сообщили мне об этой проблеме. Я хотел бы извиниться за любые неудобства, вызванные этой проблемой. Я создал внутренний отчет об ошибках, чтобы уведомить разработчиков MATLAB об этом поведении. Они могут исправить это в будущем выпуске MATLAB.

Поскольку у вас возник дополнительный вопрос о сравнении производительности кода GPU с использованием «gputimeit» и «tic» и «toc», я просто хотел дать одно предложение, которое разработчики вычислительных машин на GPU MATLAB упоминали ранее. , Обычно хорошо также вызывать «wait (gpuDevice)» перед операторами «tic», чтобы убедиться, что операции GPU из предыдущих строк не перекрываются при измерении для следующей строки. Например, в следующих строках:

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

если «wait (gpuDevice)» не вызывается перед «tic», некоторое время, затрачиваемое на создание массива «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
0 голосов
/ 01 мая 2018

РЕДАКТИРОВАТЬ 2 Возможно, я был прав, см. этот другой ответ

РЕДАКТИРОВАТЬ : они используют MAGMA , который является основным столбцом. Мой ответ не верен, однако я оставлю его здесь на некоторое время, если он поможет взломать это странное поведение.

Ниже приведен неправильный ответ

Это мое предположение, я не могу на 100% сказать вам, не зная кода под капотом MATLAB.


Гипотеза: В коде параллельных вычислений MATLABs используются библиотеки CUDA, а не их собственные.

Важная информация

  • MATLAB - основной столбец, а CUDA - основной ряд.
  • Нет таких вещей, как 2D матрицы, только 1D матрицы с 2 индексами

Почему это имеет значение? Хорошо, потому что CUDA - это высокооптимизированный код, который использует структуру памяти, чтобы максимизировать количество попаданий в кэш на ядро ​​(самая медленная операция на GPU - чтение памяти). Это означает, что стандартный код умножения матриц CUDA будет использовать порядок чтения из памяти, чтобы убедиться, что они смежны. Однако то, что является смежной памятью в мажорной строке, не в мажорной колонке.

Итак, есть 2 решения для этого, так как кто-то пишет программное обеспечение

  1. Напишите свои собственные библиотеки алгебры мажорных столбцов в CUDA
  2. Взять каждый ввод / вывод из MATLAB и транспонировать его (т.е. преобразовать из основного столбца в основной)

Они выполнили пункт 2, и предполагая, что есть интеллектуальный JIT-компилятор для набора инструментов параллельной обработки MATLAB (разумное предположение), для второго случая требуется a и b, транспонирует их, выполняет математические вычисления, и транспонирует вывод, когда вы gather.

В первом случае, однако, вам уже не нужно транспонировать вывод, так как он уже внутренне транспонирован, и JIT ловит это, поэтому вместо вызова gather(transpose( XX )) он просто пропускает выходное преобразование в сторону. То же самое с transpose(a*b). Обратите внимание, что transpose(a*b)=transpose(b)*transpose(a), поэтому внезапно транспонирование не требуется (все они пропущены внутри). Перестановка является дорогостоящей операцией.

Действительно, здесь есть странная вещь: превращение кода в функцию внезапно делает его медленным. Мое лучшее предположение состоит в том, что, поскольку JIT ведет себя по-разному в разных ситуациях, он не улавливает все эти вещи transpose внутри и все равно выполняет все операции, теряя скорость.


Интересное наблюдение: в CPU требуется столько же времени, сколько в GPU, чтобы сделать a*b*a в моем ПК.

...