Как BLAS обеспечивает такую ​​исключительную производительность?

108

Из любопытства я решил сравнить свою функцию умножения матриц с реализацией BLAS ... Результат меня, мягко говоря, удивил:

Заказная реализация, 10 попыток умножения матриц 1000x1000:

Took: 15.76542 seconds.

Внедрение BLAS, 10 попыток умножения матриц 1000x1000:

Took: 1.32432 seconds.

Здесь используются числа с плавающей запятой одинарной точности.

Моя реализация:

template<class ValT>
void mmult(const ValT* A, int ADim1, int ADim2, const ValT* B, int BDim1, int BDim2, ValT* C)
{
    if ( ADim2!=BDim1 )
        throw std::runtime_error("Error sizes off");

    memset((void*)C,0,sizeof(ValT)*ADim1*BDim2);
    int cc2,cc1,cr1;
    for ( cc2=0 ; cc2<BDim2 ; ++cc2 )
        for ( cc1=0 ; cc1<ADim2 ; ++cc1 )
            for ( cr1=0 ; cr1<ADim1 ; ++cr1 )
                C[cc2*ADim2+cr1] += A[cc1*ADim1+cr1]*B[cc2*BDim1+cc1];
}

У меня два вопроса:

  1. Учитывая, что умножение матрицы на матрицу говорит: nxm * mxn требует умножений n * n * m, поэтому в случае выше 1000 ^ 3 или 1e9 операций. Как это возможно на моем процессоре 2,6 ГГц для BLAS выполнять 10 * 1e9 операций за 1,32 секунды? Даже если умножение было одной операцией и больше ничего не делалось, это должно занять ~ 4 секунды.
  2. Почему моя реализация намного медленнее?
ДеусАдуро
источник
17
BLAS оптимизирован с одной стороны, а с другой - специалистами в этой области. Я предполагаю, что он использует преимущества модуля с плавающей запятой SIMD на вашем чипе и использует множество трюков для улучшения поведения кеширования ...
dmckee --- котенок экс-модератора
3
Тем не менее, как вы выполняете операции 1E10 на процессоре с 2,63E9 циклами в секунду за 1,3 секунды?
DeusAduro
9
Множественные исполнительные блоки, конвейерная обработка и множественные данные одной инструкции ((SIMD), что означает выполнение одной и той же операции над более чем одной парой операндов одновременно). Некоторые компиляторы могут нацеливать блоки SIMD на общие микросхемы, но вам почти всегда нужно явно включать их, и это помогает понять, как все это работает ( en.wikipedia.org/wiki/SIMD ). Страхование от промахов в кэше почти наверняка является самой сложной частью.
dmckee --- котенок экс-модератора
13
Предположение неверно. Известны алгоритмы получше, см. Википедию.
MSalters
2
@DeusAduro: В моем ответе на вопрос, как написать матричное матричное произведение, которое может конкурировать с Eigen? Я опубликовал небольшой пример того, как реализовать матрично-матричный продукт с эффективным кешированием.
Майкл Лен

Ответы:

141

Хорошей отправной точкой является прекрасная книга Роберта А. ван де Гейна и Энрике С. Кинтана-Орти «Наука о программировании матричных вычислений ». Они предоставляют бесплатную версию для загрузки.

BLAS разделен на три уровня:

  • Уровень 1 определяет набор функций линейной алгебры, которые работают только с векторами. Эти функции выигрывают от векторизации (например, от использования SSE).

  • Функции уровня 2 - это операции матрица-вектор, например, некоторое произведение матрица-вектор. Эти функции могут быть реализованы в виде функций Уровня 1. Однако вы можете повысить производительность этих функций, если можете предоставить специальную реализацию, которая использует некоторую многопроцессорную архитектуру с общей памятью.

  • Функции уровня 3 - это операции, подобные произведению матрицы на матрицу. Опять же, вы можете реализовать их в терминах функций Level2. Но функции Level3 выполняют O (N ^ 3) операций с данными O (N ^ 2). Поэтому, если ваша платформа имеет иерархию кеша, вы можете повысить производительность, если предоставите специальную реализацию, оптимизированную для кеширования / дружественную кеш-памяти . Это прекрасно описано в книге. Основным преимуществом функций Level3 является оптимизация кеша. Это повышение значительно превышает второе повышение от параллелизма и других аппаратных оптимизаций.

Кстати, большинство (или даже все) высокопроизводительных реализаций BLAS НЕ реализованы на Фортране. ATLAS реализован на C. GotoBLAS / OpenBLAS реализован на C, а его критические для производительности части - на Assembler. В Фортране реализована только эталонная реализация BLAS. Однако все эти реализации BLAS предоставляют интерфейс Fortran, так что он может быть связан с LAPACK (LAPACK получает всю свою производительность от BLAS).

Оптимизированные компиляторы играют второстепенную роль в этом отношении (а для GotoBLAS / OpenBLAS компилятор вообще не имеет значения).

ИМХО ни одна реализация BLAS не использует такие алгоритмы, как алгоритм Копперсмита – Винограда или алгоритм Штрассена. Я не совсем уверен в причине, но это мое предположение:

  • Возможно, невозможно обеспечить реализацию этих алгоритмов, оптимизированную для кеширования (т.е. вы потеряете больше, чем выиграете)
  • Эти алгоритмы численно нестабильны. Поскольку BLAS является вычислительным ядром LAPACK, это недопустимо.

Изменить / обновить:

Новым и новаторским документом по этой теме являются документы BLIS . Они исключительно хорошо написаны. Для моей лекции «Основы программного обеспечения для высокопроизводительных вычислений» я реализовал матрично-матричное произведение после их статьи. Фактически я реализовал несколько вариантов матричного произведения. Простейшие варианты полностью написаны на простом C и содержат менее 450 строк кода. Все остальные варианты просто оптимизируют циклы.

    for (l=0; l<MR*NR; ++l) {
        AB[l] = 0;
    }
    for (l=0; l<kc; ++l) {
        for (j=0; j<NR; ++j) {
            for (i=0; i<MR; ++i) {
                AB[i+j*MR] += A[i]*B[j];
            }
        }
        A += MR;
        B += NR;
    }

Общая производительность матричного продукта зависит только от этих циклов. Здесь проводится около 99,9% времени. В других вариантах я использовал встроенные функции и код ассемблера для повышения производительности. Вы можете увидеть, как в руководстве рассматриваются все варианты здесь:

ulmBLAS: Учебник по GEMM (матрично-матричный продукт)

Вместе с документами BLIS становится довольно легко понять, как библиотеки, такие как Intel MKL, могут добиться такой производительности. И почему не имеет значения, используете ли вы основное хранилище строк или столбцов!

Вот и финальные тесты (мы назвали наш проект ulmBLAS):

Тесты для ulmBLAS, BLIS, MKL, openBLAS и Eigen

Другое редактирование / обновление:

Я также написал учебник о том, как BLAS используется для решения задач численной линейной алгебры, таких как решение системы линейных уравнений:

Факторизация LU с высокими характеристиками

(Эта факторизация LU, например, используется Matlab для решения системы линейных уравнений.)

Я надеюсь найти время, чтобы расширить руководство, чтобы описать и продемонстрировать, как реализовать хорошо масштабируемую параллельную реализацию факторизации LU, как в PLASMA .

Хорошо, вот и все: кодирование оптимизированной для кэша параллельной факторизации LU

PS: Я также провел несколько экспериментов по повышению производительности uBLAS. На самом деле повысить (да, игра словами :)) производительность uBLAS довольно просто:

Эксперименты на uBLAS .

Вот похожий проект с BLAZE :

Эксперименты на BLAZE .

Майкл Лен
источник
3
Новая ссылка на «Контрольные показатели для ulmBLAS, BLIS, MKL, openBLAS и Eigen»: apfel.mathematik.uni-ulm.de/~lehn/ulmBLAS/#toc3
Ахмед Фасих
Оказывается, IBM ESSL использует вариант алгоритма Штрассена - ibm.com/support/knowledgecenter/en/SSFHY8/essl_welcome.html
ben-albrecht 01
2
большинство ссылок мертвы
Орелиен Пьер
PDF-файл TSoPMC можно найти на странице автора по адресу cs.utexas.edu/users/rvdg/tmp/TSoPMC.pdf
Алексей
Хотя алгоритм Копперсмита-Винограда имеет приятную временную сложность на бумаге, нотация Big O скрывает очень большую константу, поэтому он начинает становиться жизнеспособным только для смехотворно больших матриц.
Нихар Карве,
26

Итак, прежде всего, BLAS - это просто интерфейс, содержащий около 50 функций. Существует множество конкурирующих реализаций интерфейса.

Во-первых, я упомяну вещи, которые в значительной степени не связаны:

  • Фортран против C, без разницы
  • Расширенные матричные алгоритмы, такие как Strassen, реализации не используют их, поскольку они не помогают на практике

Большинство реализаций разбивают каждую операцию на матричные или векторные операции малого размера более или менее очевидным образом. Например, большое матричное умножение 1000x1000 может быть разбито на последовательность умножений матриц 50x50.

Эти операции фиксированного размера и небольшого размера (называемые ядрами) жестко запрограммированы в специфичном для ЦП ассемблерном коде с использованием нескольких функций ЦП своей цели:

  • Инструкции в стиле SIMD
  • Параллелизм на уровне инструкций
  • Осведомленность о кэше

Кроме того, эти ядра могут выполняться параллельно друг другу с использованием нескольких потоков (ядер ЦП) в типичном шаблоне проектирования map-reduce.

Взгляните на ATLAS, который является наиболее часто используемой реализацией BLAS с открытым исходным кодом. У него много разных конкурирующих ядер, и в процессе сборки библиотеки ATLAS он конкурирует между ними (некоторые даже параметризованы, поэтому одно и то же ядро ​​может иметь разные настройки). Он пробует разные конфигурации, а затем выбирает лучшую для конкретной целевой системы.

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

Эндрю Томазос
источник
ATLAS больше не является наиболее часто используемой реализацией BLAS с открытым исходным кодом. Его превзошли OpenBLAS (ответвление GotoBLAS) и BLIS (рефакторинг GotoBLAS).
Роберт ван де Гейн
1
@ ulaff.net: Возможно. Это было написано 6 лет назад. Я думаю, что самая быстрая реализация BLAS в настоящее время (на Intel, конечно) - это Intel MKL, но это не открытый исходный код.
Эндрю Томазос
14

Во-первых, есть более эффективные алгоритмы умножения матриц, чем тот, который вы используете.

Во-вторых, ваш процессор может выполнять гораздо больше, чем одну инструкцию за раз.

Ваш ЦП выполняет 3-4 инструкции за цикл, и если используются блоки SIMD, каждая инструкция обрабатывает 4 числа с плавающей запятой или 2 двойных. (конечно, эта цифра также не точна, поскольку ЦП обычно может обрабатывать только одну инструкцию SIMD за цикл)

В-третьих, ваш код далек от оптимального:

  • Вы используете необработанные указатели, что означает, что компилятор должен предположить, что они могут быть псевдонимами. Существуют специфичные для компилятора ключевые слова или флаги, которые вы можете указать, чтобы сообщить компилятору, что они не являются псевдонимами. В качестве альтернативы вы должны использовать другие типы, кроме необработанных указателей, которые решают проблему.
  • Вы забиваете кеш, выполняя наивный обход каждой строки / столбца входных матриц. Вы можете использовать блокировку, чтобы выполнить как можно больше работы с меньшим блоком матрицы, который умещается в кеш-памяти ЦП, перед переходом к следующему блоку.
  • Для чисто числовых задач Фортран в значительной степени непобедим, а С ++ требует много усилий, чтобы достичь такой же скорости. Это можно сделать, и есть несколько библиотек, демонстрирующих это (обычно с использованием шаблонов выражений), но это нетривиально, и это не происходит просто так.
Jalf
источник
Спасибо, я добавил ограничить правильный код в соответствии с предложением Justicle, особых улучшений не увидел, мне нравится блочная идея. Из любопытства, не зная размера кеш-памяти процессора, как правильно выбрать оптимальный код?
DeusAduro
2
Вы этого не сделаете. Чтобы получить оптимальный код, вам необходимо знать размер кеш-памяти процессора. Конечно, недостатком этого является то, что вы эффективно жестко кодируете свой код для обеспечения максимальной производительности на одном семействе процессоров.
jalf
2
По крайней мере, внутренний контур здесь позволяет избежать перегрузок. Похоже, это написано для одной уже транспонированной матрицы. Вот почему он «всего» на порядок медленнее, чем BLAS! Но да, по-прежнему ломается из-за отсутствия блокировки кеша. Вы уверены, что Фортран сильно поможет? Я думаю, все, что вы здесь получите, это то, что restrict(без псевдонима) по умолчанию, в отличие от C / C ++. (И, к сожалению, в ISO C ++ нет restrictключевого слова, поэтому вы должны использовать его __restrict__в компиляторах, которые предоставляют его как расширение).
Питер Кордес
11

Я не знаю конкретно о реализации BLAS, но есть более эффективные алгоритмы для умножения матриц, которые имеют сложность выше O (n3). Хорошо известный алгоритм Штрассена

софтведа
источник
8
Алгоритм Штрассена не используется в числах по двум причинам: 1) Он нестабилен. 2) Вы экономите некоторые вычисления, но за это приходится расплачиваться за использование иерархий кеша. На практике вы даже теряете производительность.
Майкл Лен
4
Для практической реализации алгоритма Штрассена, основанного на исходном коде библиотеки BLAS, есть недавняя публикация: « Перезагрузка алгоритма Штрассена » в SC16, которая обеспечивает более высокую производительность, чем BLAS, даже для размера задачи 1000x1000.
Jianyu Huang
4

Большинство аргументов ко второму вопросу - ассемблер, разбиение на блоки и т.д. (но не менее N ^ 3 алгоритмов, они действительно чрезмерно развиты) - играют роль. Но низкая скорость вашего алгоритма вызвана в основном размером матрицы и неудачным расположением трех вложенных циклов. Ваши матрицы настолько велики, что не помещаются сразу в кеш-память. Вы можете переупорядочить циклы таким образом, чтобы как можно больше было сделано для строки в кеше, таким образом резко уменьшая количество обновлений кеша (BTW разделение на небольшие блоки имеет аналогичный эффект, лучше всего, если циклы по блокам расположены аналогично). Ниже приводится реализация модели для квадратных матриц. На моем компьютере его расход времени был примерно 1:10 по сравнению со стандартной реализацией (как у вас). Другими словами: никогда не программируйте умножение матриц по "

    void vector(int m, double ** a, double ** b, double ** c) {
      int i, j, k;
      for (i=0; i<m; i++) {
        double * ci = c[i];
        for (k=0; k<m; k++) ci[k] = 0.;
        for (j=0; j<m; j++) {
          double aij = a[i][j];
          double * bj = b[j];
          for (k=0; k<m; k++)  ci[k] += aij*bj[k];
        }
      }
    }

Еще одно замечание: эта реализация на моем компьютере даже лучше, чем замена всех процедурой BLAS cblas_dgemm (попробуйте ее на своем компьютере!). Но гораздо быстрее (1: 4) вызывает dgemm_ библиотеки Fortran напрямую. Я думаю, что это на самом деле не Фортран, а код ассемблера (я не знаю, что находится в библиотеке, у меня нет исходников). Мне совершенно непонятно, почему cblas_dgemm работает не так быстро, поскольку, насколько мне известно, это просто оболочка для dgemm_.

Вольфганг Янсен
источник
3

Это реальная скорость. Для примера того, что можно сделать с помощью ассемблера SIMD поверх кода C ++, см. Некоторые примеры матричных функций iPhone - они были более чем в 8 раз быстрее, чем версия C, и даже не были «оптимизированной» сборкой - пока нет конвейерной обработки это ненужные стековые операции.

Также ваш код не является « ограничивающим правильным » - как компилятор узнает, что когда он изменяет C, он не изменяет A и B?

Justicle
источник
Конечно, если вы вызвали такую ​​функцию, как mmult (A ..., A ..., A); вы точно не получите ожидаемого результата. Опять же, хотя я не пытался превзойти / повторно внедрить BLAS, просто смотрел, насколько он на самом деле быстр, поэтому проверка ошибок не имела в виду, только базовые функции.
DeusAduro
3
Извините, чтобы внести ясность, я хочу сказать, что если вы поставите "ограничить" свои указатели, вы получите гораздо более быстрый код. Это связано с тем, что каждый раз, когда вы изменяете C, компилятору не нужно перезагружать A и B, что значительно ускоряет внутренний цикл. Если не верите, проверьте разборку.
Justicle
@DeusAduro: это не проверка ошибок - возможно, что компилятор не может оптимизировать доступ к массиву B [] во внутреннем цикле, потому что он не сможет определить, что указатели A и C никогда не являются псевдонимом B массив. Если бы существовал псевдоним, значение в массиве B могло бы измениться во время выполнения внутреннего цикла. Поднятие доступа к значению B [] из внутреннего цикла и помещение его в локальную переменную может позволить компилятору избежать постоянного доступа к B [].
Майкл Берр,
1
Хммм, поэтому я сначала попробовал использовать ключевое слово __restrict в VS 2008, примененное к A, B и C. Результат не изменился. Однако перемещение доступа к B из самого внутреннего цикла в цикл снаружи улучшило время на ~ 10%.
DeusAduro
1
Извините, я не уверен насчет ВК, но с GCC нужно включить -fstrict-aliasing. Здесь также есть лучшее объяснение «ограничения»: cellperformance.beyond3d.com/articles/2006/05/…
Justicle
2

По отношению к исходному коду в умножении MM обращение к памяти для большинства операций является основной причиной плохой производительности. Память работает в 100-1000 раз медленнее, чем кеш.

Большая часть ускорения достигается за счет использования методов оптимизации цикла для этой функции тройного цикла в умножении MM. Используются два основных метода оптимизации цикла; разворачивание и блокировка. Что касается разворачивания, мы разворачиваем два внешних цикла и блокируем его для повторного использования данных в кеше. Развертывание внешнего цикла помогает оптимизировать доступ к данным во времени за счет уменьшения количества ссылок на одни и те же данные в разное время в течение всей операции. Блокирование индекса цикла по определенному номеру помогает сохранить данные в кеше. Вы можете выбрать оптимизацию для кеш-памяти второго или третьего уровня.

https://en.wikipedia.org/wiki/Loop_nest_optimization

Пари Раджарам
источник
-24

По многим причинам.

Во-первых, компиляторы Fortran сильно оптимизированы, и язык позволяет им быть такими. C и C ++ очень свободны с точки зрения обработки массивов (например, в случае указателей, относящихся к одной и той же области памяти). Это означает, что компилятор не может заранее знать, что делать, и вынужден создавать общий код. В Фортране ваши случаи более оптимизированы, а компилятор лучше контролирует происходящее, что позволяет ему больше оптимизировать (например, с помощью регистров).

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

В-третьих, это зависит от используемой вами реализации blas. Некоторые реализации могут быть написаны на ассемблере и оптимизированы для конкретного процессора, который вы используете. Версия netlib написана на fortran 77.

Кроме того, вы выполняете множество операций, большинство из которых повторяются и избыточны. Все эти умножения для получения индекса пагубно сказываются на производительности. Я действительно не знаю, как это делается в BLAS, но есть много уловок для предотвращения дорогостоящих операций.

Например, вы можете переработать свой код таким образом

template<class ValT>
void mmult(const ValT* A, int ADim1, int ADim2, const ValT* B, int BDim1, int BDim2, ValT* C)
{
if ( ADim2!=BDim1 ) throw std::runtime_error("Error sizes off");

memset((void*)C,0,sizeof(ValT)*ADim1*BDim2);
int cc2,cc1,cr1, a1,a2,a3;
for ( cc2=0 ; cc2<BDim2 ; ++cc2 ) {
    a1 = cc2*ADim2;
    a3 = cc2*BDim1
    for ( cc1=0 ; cc1<ADim2 ; ++cc1 ) {
          a2=cc1*ADim1;
          ValT b = B[a3+cc1];
          for ( cr1=0 ; cr1<ADim1 ; ++cr1 ) {
                    C[a1+cr1] += A[a2+cr1]*b;
           }
     }
  }
} 

Попробуй, уверен, что-нибудь сэкономишь.

На ваш вопрос №1 причина в том, что умножение матриц масштабируется как O (n ^ 3), если вы используете тривиальный алгоритм. Есть алгоритмы, которые масштабируются намного лучше .

Стефано Борини
источник
36
Это совершенно неверный ответ, извините. Реализации BLAS не написаны на fortran. Код, критичный к производительности, написан на ассемблере, а наиболее распространенные в наши дни написаны на C выше. Также BLAS определяет порядок строк / столбцов как часть интерфейса, и реализации могут обрабатывать любую комбинацию.
Эндрю Томазос
10
Да, этот ответ является совершенно неправильным. К сожалению, он полон здравого смысла, например, утверждение, что BLAS быстрее благодаря Фортрану. Иметь 20 (!) Положительных оценок - это плохо. Теперь эта чушь распространяется еще больше из-за популярности Stackoverflow!
Майкл Лен
12
Я думаю, вы путаете неоптимизированную эталонную реализацию с производственными реализациями. Эталонная реализация предназначена только для указания интерфейса и поведения библиотеки и была написана на Фортране по историческим причинам. Это не для производственного использования. В производстве люди используют оптимизированные реализации, которые демонстрируют то же поведение, что и эталонная реализация. Я изучил внутреннее устройство ATLAS (который поддерживает Octave - Linux "MATLAB"), который, как я могу подтвердить, внутренне написан на C / ASM. Коммерческие реализации почти наверняка тоже.
Эндрю Томазос
5
@KyleKanos: Да, вот источник ATLAS: sourceforge.net/projects/math-atlas/files/Stable/3.10.1 Насколько мне известно, это наиболее часто используемая переносимая реализация BLAS с открытым исходным кодом. Он написан на C / ASM. Производители высокопроизводительных процессоров, такие как Intel, также предоставляют реализации BLAS, специально оптимизированные для своих чипов. Я гарантирую, что на низком уровне части библиотеки Intel написаны на (duuh) сборке x86, и я почти уверен, что части среднего уровня будут написаны на C или C ++.
Эндрю Томазос 05
9
@KyleKanos: Вы в замешательстве. Netlib BLAS - эталонная реализация. Эталонная реализация намного медленнее, чем оптимизированные реализации (см. Сравнение производительности ). Когда кто-то говорит, что использует netlib BLAS в кластере, это не означает, что они на самом деле используют эталонную реализацию netlib. Это было бы просто глупо. Это просто означает, что они используют библиотеку с тем же интерфейсом, что и blas-библиотека netlib.
Эндрю Томазос 05