Скорость SVD в CPU и GPU

Я тестирую svd в Matlab R2014a, и кажется, что CPU против GPU ускорения нет. Я использую карту GTX 460 и карту Core 2 duo E8500.

Вот мой код:

%test SVD
n=10000;
%host
Mh= rand(n,1000);
tic
%[Uh,Sh,Vh]= svd(Mh);
svd(Mh);
toc
%device
Md = gpuArray.rand(n,1000);
tic
%[Ud,Sd,Vd]= svd(Md);
svd(Md);
toc

Кроме того, время выполнения отличается от запуска к запуску, но версии CPU и GPU примерно одинаковы. Почему нет ускорения?

Вот несколько тестов

for i=1:10
    clear;
    m= 10000;
    n= 100;
    %host
    Mh= rand(m,n);
    tic
    [Uh,Sh,Vh]= svd(Mh);
    toc
    %device
    Md = gpuArray.rand(m,n);
    tic
    [Ud,Sd,Vd]= svd(Md);
    toc
end

>> test_gpu_svd
Elapsed time is 43.124130 seconds.
Elapsed time is 43.842277 seconds.
Elapsed time is 42.993283 seconds.
Elapsed time is 44.293410 seconds.
Elapsed time is 42.924541 seconds.
Elapsed time is 43.730343 seconds.
Elapsed time is 43.125938 seconds.
Elapsed time is 43.645095 seconds.
Elapsed time is 43.492129 seconds.
Elapsed time is 43.459277 seconds.
Elapsed time is 43.327012 seconds.
Elapsed time is 44.040959 seconds.
Elapsed time is 43.242291 seconds.
Elapsed time is 43.390881 seconds.
Elapsed time is 43.275379 seconds.
Elapsed time is 43.408705 seconds.
Elapsed time is 43.320387 seconds.
Elapsed time is 44.232156 seconds.
Elapsed time is 42.984002 seconds.
Elapsed time is 43.702430 seconds.


for i=1:10
    clear;
    m= 10000;
    n= 100;
    %host
    Mh= rand(m,n,'single');
    tic
    [Uh,Sh,Vh]= svd(Mh);
    toc
    %device
    Md = gpuArray.rand(m,n,'single');
    tic
    [Ud,Sd,Vd]= svd(Md);
    toc
end

>> test_gpu_svd
Elapsed time is 21.140301 seconds.
Elapsed time is 21.334361 seconds.
Elapsed time is 21.275991 seconds.
Elapsed time is 21.582602 seconds.
Elapsed time is 21.093408 seconds.
Elapsed time is 21.305413 seconds.
Elapsed time is 21.482931 seconds.
Elapsed time is 21.327842 seconds.
Elapsed time is 21.120969 seconds.
Elapsed time is 21.701752 seconds.
Elapsed time is 21.117268 seconds.
Elapsed time is 21.384318 seconds.
Elapsed time is 21.359225 seconds.
Elapsed time is 21.911570 seconds.
Elapsed time is 21.086259 seconds.
Elapsed time is 21.263040 seconds.
Elapsed time is 21.472175 seconds.
Elapsed time is 21.561370 seconds.
Elapsed time is 21.330314 seconds.
Elapsed time is 21.546260 seconds.

person mrgloom    schedule 07.11.2014    source источник
comment
Какое время выполнения вы получаете? Какая у вас модель процессора? Вы запускали этот тестовый код несколько раз, чтобы разогреть GPU, прежде чем записывать время выполнения?   -  person Divakar    schedule 07.11.2014
comment
Кроме того, для справедливого бенчмаркинга, я думаю, вам нужно использовать gather, т.е. gather(svd(Md)).   -  person Divakar    schedule 07.11.2014
comment
svd(Md) вычисляет только сингулярные значения, 1000 для имеющейся матрицы. У вас все еще нет ускорения при использовании [Ud,Sd,Vd]= svd(Md)?   -  person Vitality    schedule 07.11.2014
comment
Обратите внимание, что версия функции Matlab для процессора также хорошо оптимизирована и, возможно, использует несколько ядер за сценой, если у вас есть набор инструментов для параллельных вычислений. Таким образом, Matlab обычно не является хорошим программным обеспечением для демонстрации ускорения графического процессора.   -  person Tae-Sung Shin    schedule 07.11.2014


Ответы (4)


Как правило, SVD трудно распараллелить. Вы можете проверить здесь что с высокопроизводительной картой Tesla ускорение не очень впечатляет.

У вас есть карта GTX460 — архитектура Fermi . Карта оптимизирована для игр (вычисления с одинарной точностью), а не для HPC (вычисления с двойной точностью). Соотношение пропускной способности Single Precision/Double Precision равно 12. Таким образом, карта имеет 873 GFLOPS SP / 72 GFLOPS DP. Проверьте здесь.

Так что, если массив Md использует элементы двойной точности, то вычисления на нем будут довольно медленными. Также существует высокая вероятность того, что при вызове подпрограммы ЦП будут задействованы все ядра ЦП, что снижает возможный выигрыш от выполнения подпрограммы на графическом процессоре. Кроме того, при запуске GPU вы платите время за передачу буфера на устройство.

По предложению Дивакара вы можете использовать Md = single(Md) для преобразования вашего массива в одинарную точность и снова запустить тест. Вы можете попробовать увеличить размер набора данных, чтобы увидеть, изменится ли что-то. Я не ожидаю большого выигрыша от этой процедуры на вашем графическом процессоре.

Обновление 1:

После того, как вы опубликовали результаты, я увидел, что соотношение времени DP/SP равно 2. На стороне процессора это нормально, потому что в SSE-регистрах можно разместить в 2 раза меньше double значений. Тем не менее, соотношение только 2 на стороне графического процессора означает, что код графического процессора не использует ядра SM наилучшим образом, потому что теоретическое соотношение равно 12. Другими словами, я ожидал гораздо более высокой производительности SP для оптимизированный код по сравнению с DP. Кажется, что это не так.

person VAndrei    schedule 07.11.2014
comment
Чтобы расширить утверждение о одинарной/двойной точности, вы можете предложить использовать Md = single(Md) перед синхронизацией кодов GPU, если небольшая потеря точности подходит для OP. - person Divakar; 07.11.2014
comment
@Дивакар Спасибо! Я добавил комментарий к посту. - person VAndrei; 07.11.2014
comment
Донгарра и его соавторы описывают некоторые проблемы с ускорением SVD на графических процессорах в Ускорение вычислений численной плотной линейной алгебры с помощью графических процессоров. Цитата: Есть много способов математически сформулировать и решить эти задачи численно, но во всех случаях разработка эффективных вычислений является сложной задачей из-за природы алгоритмов. - person njuffa; 08.11.2014

Как уже говорил В.Андрей, SVD — алгоритм, который сложно распараллелить.

Ваша основная проблема заключается в размере вашей матрицы. Производительность СВД быстро падает с ростом размера матрицы. Поэтому вашей главной целью должно быть уменьшение размера матрицы. Это можно сделать с помощью нормальных уравнений Гаусса (которые в основном представляют собой редукцию переопределенной линейной системы в смысле метода наименьших квадратов).

Это можно сделать, просто умножив транспонирование на матрицу:

MhReduced = Mh' * Mh;

Это уменьшает вашу матрицу до размера cols*cols (если cols — это количество столбцов Mh). Тогда вы просто позвоните [U,S,V] = svd(MhReduced);

Примечание. Использование этого метода может привести к сингулярным векторам с противоположным знаком (это важно, если вы сравниваете эти методы).

Если ваш матикс хорошо кондиционирован, это должно работать без проблем. Однако в случае плохо обусловленной матрицы этот метод может не дать полезного результата, тогда как непосредственное применение SVD все же может дать полезный результат из-за надежности SVD.

Это должно значительно увеличить вашу производительность, по крайней мере, с достаточно большими матрицами. Еще одним преимуществом является то, что вы можете использовать гораздо большие матрицы. Вам, вероятно, вообще не придется использовать GPU (поскольку либо матрицы настолько велики, что копирование на GPU стоит слишком дорого, либо после сокращения матрица настолько мала, что ускорение GPU не будет достаточно большим).

Также обратите внимание, что большая часть производительности теряется, если вы используете возвращаемые значения. Если вас интересует только производительность вычисления SVD, не принимайте никаких возвращаемых значений. Если вас интересует только «вектор решения», просто получите V (и получите доступ к последнему столбцу): [~,~, V] = svd(Mh);.

РЕДАКТИРОВАТЬ:

Я посмотрел на ваш пример кода, но я не уверен, что это такое, вы рассчитываете. Также я понял, что довольно сложно понять, что я сделал с A'*A, поэтому объясню подробно.

Для линейной системы с A*x=b, где A обозначает матрицу коэффициентов с m строками и n столбцами, x - вектор решения, а b - постоянный вектор (оба с m строками), решение можно вычислить следующим образом:

  • если A квадрат (m=n): x = A^-1 * b,
  • если A не квадрат (m!=n, m > n):

    A * x = b

    A'* A * x = A' * b

    x = (A' * A)^-1 * A'*b

A" = (A'*A)^-1 * A' обычно называют псевдоинверсией. Однако этот расчет отрицательно влияет на число обусловленности матрицы. Решением этой проблемы является использование разложения по сингулярным числам (SVD). Если USV = svd(A) обозначает результаты SVD, псевдообратное значение задается как VS"U', при этом S" формируется путем взятия обратного значения ненулевых элементов S. Итак, A" = VS"U'.

x = A"*b

Однако поскольку СВД довольно затратна, особенно с большими матрицами. Если матрица A хорошо обусловлена ​​и очень точные результаты не требуются (мы говорим о 1e-13 или 1e-14), можно использовать гораздо более быстрый подход, вычисляя псевдоинверсию через (A'*A)^-1 * A.

Если ваш случай на самом деле A*x=0, просто используйте SVD и прочитайте последний вектор-столбец из V, это решение.

Если вы используете SVD не для решения линейной системы, а для результатов U и S (как предполагает ваш пример), я не уверен, что то, что я опубликовал, вам поможет.

Источники: 1, 2, 3

Вот пример кода для тестирования. Протестируйте его с большими матрицами, вы увидите, что использование (A'*A)^-1 * A' намного быстрее, чем альтернативы.

clear all

nbRows = 30000;
nbCols = 100;
% Matrix A
A = rand(nbRows,nbCols);

% Vector b
b = rand(nbRows,1);

% A*x=b

% Solve for x, using SVD
% [U,S,V]=svd(A,0);
% x= V*((U'*b)./diag(S))
tic
[U1,S1,V1]=svd(A,0);
x1= V1*((U1'*b)./diag(S1));
toc

tic
[U1,S1,V1]=svd(A,0);
x2 = V1*inv(S1)*U1'*b;
toc

% Solve for x, using manual pseudo-inverse
% A*x=b
% A'*A*x = A'*b
% x = (A'*A)^-1 * A'*b
tic
x3 = inv(A'*A) * A'*b;
toc

% Solve for x, let Matlab decide how (most likely SVD)
tic
x4 = A\b;
toc
person Baiz    schedule 08.11.2014
comment
Как это MhReduced = Mh' * Mh; трюк называется? Это что-то вроде ковариационной матрицы? - person mrgloom; 09.11.2014
comment
Я знаю это только по названию нормальных уравнений. У меня мало знаний о матрицах и линейных уравнениях, но это может помочь: math.wsu.edu/faculty/genz/448/lessons/l401.pdf - person Baiz; 09.11.2014
comment
кажется, что a= rand(3,2) svd(a) svd(a'*a) дает разные ответы. - person mrgloom; 09.11.2014
comment
Да, U и S разные. Однако V кажется идентичным (за исключением очень маленького эпсилон, в зависимости от размера матрицы), и в большинстве случаев вы используете SVD для доступа к последнему вектору-столбцу в V (поскольку это вектор, соответствующий наименьшему сингулярному значению и представляющий лучшее решение линейной системы). Однако, если вам нужны U и S, я не знаю, как сохранить их с помощью этого метода. Какие части результатов СВД вы используете? - person Baiz; 09.11.2014
comment
что-то вроде этого gist.github.com/mrgloom/3943410759f04265f7cb также проверьте a= rand(3, 2);svd(a)-svd(a'*a) самостоятельно. - person mrgloom; 13.11.2014
comment
Я обновил свой ответ, чтобы прояснить, как можно решить линейные системы с помощью A'*A. Дайте мне знать, если это поможет вашим вещам. - person Baiz; 13.11.2014
comment
Я не использую SVD для решения A*x=b, но использую SVD для вычисления PCA. Во всяком случае, в вашем примере вы не поставили A'*A непосредственно на SVD. - person mrgloom; 14.11.2014
comment
Если вы просто решаете линейные уравнения в форме A*x=0, вас в любом случае интересует только V, поэтому вызов svd(A) эквивалентен svd(A'*A) (за исключением числовой стабильности). Поскольку вы выполняете PCA, вас интересуют сингулярные векторы (содержащиеся в V) и соответствующие сингулярные значения (диагональ V). При вызове [U1,S1,V1] = svd(A,0); и [U2,S2,V2] = svd(A'*A); V1 и V2 идентичны, а S1 и S2 отличаются только тем, что S1 = sqrt(S2). - person Baiz; 14.11.2014
comment
Хорошо, я понимаю, но столбцы матрицы V1 и V2 могут отличаться знаком +. - person mrgloom; 14.11.2014
comment
Это эффект A'*A (что происходит только иногда), но это не имеет значения, поскольку сингулярный вектор все еще действителен, если его умножить на -1 (сейчас не могу найти источник этого). Однако после того, как я прочитал об анализе PCA, я немного запутался, особенно в роли SVD. В любом случае, если вы хотите вычислить главные компоненты, есть два классических способа: 1. eigenVectors = princomp(A); 2. [собственные векторы, собственные значения] = eig(cov(A)); В настоящее время я получаю разные результаты при использовании SVD. Может быть, я делаю что-то не так. - person Baiz; 14.11.2014
comment
Хорошо, я узнал, в чем проблема. Я использовал нецентрированный набор данных (как и вы, судя по всему). Это приведет к неправильным сингулярным числам, векторы будут правильными. Если вы планируете использовать также сингулярные значения, вам придется исправить эту ошибку, либо используя princomp(A) (центрирует сами данные), либо передав матрицу ковариации собственной функции: eig(covarianceMat). Однако, если вы просто используете SVD(A), сингулярные значения и векторы!!! просто неверны. - person Baiz; 14.11.2014
comment
Краткая версия: SVD (A) не даст правильных основных компонентов, вам нужно передать либо центрированный набор данных (в основном с центрированием по строкам), либо саму ковариационную матрицу в SVD. Или вы можете просто использовать eig(cov(A)). - person Baiz; 14.11.2014
comment
math.stackexchange.com/questions/3869/ - person mrgloom; 17.11.2014
comment
Я прочитал несколько статей на эту тему, включая ссылку, которую вы разместили. Обратите внимание, что в используемом здесь примере предполагается, что набор данных имеет нулевое среднее значение. Это не случай, когда вы создаете маты с помощью M = rand(n,m); Вы должны сначала унизить свой набор данных (см. мой последний комментарий). Также обратите внимание, что ковариационная матрица задается как matCovar = (X'*X)/(nbRows-1), если столбцы являются переменными, а строки являются наблюдениями. Не уверен, почему это обрабатывается по-разному в ссылке, которую вы разместили. - person Baiz; 17.11.2014

Проблема

Прежде всего, я воспроизвел вашу проблему в Matlab2016b, используя следующий код:

clear all
close all
clc

Nrows = 2500;
Ncols = 2500;

NumTests = 10;

h_A = rand(Nrows, Ncols);
d_A = gpuArray.rand(Nrows, Ncols);

timingCPU = 0;
timingGPU = 0;

for k = 1 : NumTests
    % --- Host
    tic
    [h_U, h_S, h_V] = svd(h_A);
%     h_S = svd(h_A);
    timingCPU = timingCPU + toc;

    % --- Device
    tic
    [d_U, d_S, d_V] = svd(d_A);
%     d_S = svd(d_A);
    timingGPU = timingGPU + toc;
end

fprintf('Timing CPU = %f; Timing GPU = %f\n', timingCPU / NumTests, timingGPU / NumTests);

С помощью приведенного выше кода можно либо вычислить только сингулярные значения, либо вычислить полный SVD, включая сингулярные векторы. Также можно сравнить различное поведение CPU и GPU версий кода SVD.

Время указано в следующей таблице (время в s; Intel Core i7-6700K CPU @ 4.00GHz, 16288 MB, Max threads(8), GTX 960):

              Sing. values only | Full SVD         | Sing. val. only | Full
                                |                  |                 |
Matrix size   CPU      GPU      | CPU       GPU    |                 |
                                |                  |                 |
 200 x  200   0.0021    0.043   |  0.0051    0.024 |   0.098         |  0.15
1000 x 1000   0.0915    0.3     |  0.169     0.458 |   0.5           |  2.3
2500 x 2500   3.35      2.13    |  4.62      3.97  |   2.9           |  23
5000 x 5000   5.2      13.1     | 26.6      73.8   |  16.1           | 161

Первые столбцы 4 относятся к сравнению между версиями Matlab CPU и GPU подпрограммы svd, когда она используется для вычисления только сингулярных значений или полного SVD. Как видно, версия на GPU может быть значительно медленнее версии на GPU. Мотивация уже была указана в некоторых ответах выше: существует неотъемлемая сложность распараллеливания вычислений SVD.

Используете cuSOLVER?

На данный момент возникает очевидный вопрос: можем ли мы получить некоторое ускорение с помощью cuSOLVER? Действительно, мы могли бы использовать mexFiles, чтобы подпрограммы cuSOLVER работали под Matlab. К сожалению, ситуация с cuSOLVER еще хуже, о чем можно судить по последним двум столбцам вышеприведенной таблицы. Такие столбцы сообщают о времени кодов в Расчет сингулярных значений только с CUDA и Параллельная реализация для нескольких SVD с использованием CUDA с использованием cusolverDnSgesvd для расчета только сингулярных значений и полного расчета SVD соответственно. Как видно, cusolverDnSgesvd cuSOLVER работает даже хуже, чем Matlab, если учесть, что он работает с одинарной точностью, а Matlab с двойной точностью.

Мотивация такого поведения дополнительно объясняется в производительность cusolverDnCgesvd по сравнению с MKL, где говорит Джо Итон, менеджер библиотеки cuSOLVER

Я понимаю путаницу здесь. Мы обеспечиваем приличное ускорение для факторизаций LU, QR и LDL^t, то же самое мы хотели бы сказать и для SVD. Наша цель с cuSOLVER состоит в том, чтобы предоставить плотные и разреженные прямые решатели как часть инструментария CUDA в первый раз; мы должны с чего-то начать. Поскольку CULA больше не поддерживается, мы сочли необходимым передать некоторые функции разработчикам CUDA 7.0. Поскольку в настоящее время CUDA работает на более чем x86 хостах CPUs, cuSOLVER заполняет потребность там, где нет MKL. При этом мы можем добиться большего успеха с SVD, но придется подождать следующего релиза CUDA, поскольку приоритеты и сроки уже сжаты.

Использование других библиотек

На данный момент другими возможностями являются использование других библиотек, таких как

  1. CULA;
  2. MAGMA;
  3. ArrayFire.

CULA не предлагается бесплатно, поэтому я не пробовал.

У меня были некоторые проблемы с установкой с зависимостями MAGMA, поэтому я не исследовал этот момент дальше (отказ от ответственности: я ожидаю, что со временем я смогу решить такие проблемы).

Затем я, наконец, остановился на использовании ArrayFire.

Используя ArrayFire, у меня было следующее время для полного вычисления SVD:

 200 x  200      0.036
1000 x 1000      0.2
2500 x 2500      4.5
5000 x 5000     29

Как видно, тайминги немного выше, но уже сравнимы со случаем процессора.

Вот код ArrayFire:

#include <arrayfire.h>
#include <cstdio>
#include <cstdlib>
#include <fstream>

using namespace af;

int main(int argc, char *argv[])
{
    const int N = 1000;

    try {

        // --- Select a device and display arrayfire info
        int device = argc > 1 ? atoi(argv[1]) : 0;
        af::setDevice(device);
        af::info();

        array A = randu(N, N, f64);
        af::array U, S, Vt;

        // --- Warning up
        timer time_last = timer::start();
        af::svd(U, S, Vt, A);
        S.eval();
        af::sync();
        double elapsed = timer::stop(time_last);
        printf("elapsed time using start and stop = %g ms \n", 1000.*elapsed);

        time_last = timer::start();
        af::svd(U, S, Vt, A);
        S.eval();
        af::sync();
        elapsed = timer::stop(time_last);
        printf("elapsed time using start and stop = %g ms \n", 1000.*elapsed);

    }
    catch (af::exception& e) {

        fprintf(stderr, "%s\n", e.what());
        throw;
    }

    return 0;
}
person Vitality    schedule 15.05.2017

Я пытался распараллелить SVD на своем ноутбуке с GTX 460 в течение более одного месяца, что также было частью моей дипломной работы, я провел так много экспериментов, что позже обнаружил, что MATLAB чрезвычайно быстр и, кстати, превосходит мой код. , я использовал одну сторону Якоби, и я еще не видел ни одной статьи, раскрывающей алгоритм быстрее, чем svd в MATLAB. На графическом процессоре затраты времени на копирование памяти могут быть очень высокими, если вы не используете элегантную модель. Я отсылаю вас к дополнительной информации о CUDA. Если вам нужна помощь, пожалуйста, свяжитесь со мной.

person kzm11    schedule 28.10.2016
comment
Это должен быть не ответ, а комментарий. :) - person pix; 28.10.2016