Як 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 - це матричні векторні операції, наприклад, деякі матричні векторні вироби. Ці функції можуть бути реалізовані з точки зору функцій Level1. Однак ви можете підвищити продуктивність цих функцій, якщо зможете забезпечити спеціальну реалізацію, яка використовує деяку багатопроцесорну архітектуру зі спільною пам'яттю.

  • Функції рівня 3 - це операції, подібні до матрично-матричного продукту. Знову ви зможете їх реалізувати з точки зору функцій рівня 2 Але функції Level3 виконують операції O (N ^ 3) над даними O (N ^ 2). Отже, якщо на вашій платформі є ієрархія кешу, ви можете підвищити продуктивність, якщо забезпечите спеціальну реалізацію, оптимізовану під кеш / кеш . Це добре описано в книзі. Основний приріст функцій Level3 - це оптимізація кешу. Цей приріст значно перевершує друге поштовх за рахунок паралелізму та інших апаратних оптимізацій.

До речі, більшість (або навіть усі) високоефективних реалізацій BLAS НЕ реалізовані у Fortran. ATLAS реалізований в C. GotoBLAS / OpenBLAS реалізований в C, а його критичні показники - в Assembler. У Fortran реалізована лише опорна реалізація BLAS. Однак усі ці реалізації BLAS забезпечують інтерфейс Fortran таким, що його можна пов’язати з LAPACK (LAPACK отримує всю свою ефективність від BLAS).

Оптимізовані компілятори відіграють незначну роль у цьому відношенні (а для GotoBLAS / OpenBLAS компілятор взагалі не має значення).

Реалізація IMHO no BLAS використовує такі алгоритми, як алгоритм Coppersmith – Winograd або алгоритм Strassen. Я не точно впевнений у причині, але це моя здогадка:

  • Можливо, неможливо забезпечити оптимізовану кешами реалізацію цих алгоритмів (тобто ви втратите більше, ніж ви виграєте)
  • Ці алгоритми чисельно не стійкі. Оскільки BLAS - це обчислювальне ядро ​​LAPACK, це не працює.

Редагування / оновлення:

Новим та першочерговим документом для цієї теми є документи BLIS . Вони винятково добре написані. На моїй лекції "Основи програмного забезпечення для високоефективних обчислень" я реалізував матричний матричний продукт, слідуючи їхньому документу. Насправді я реалізував кілька варіантів матрично-матричного продукту. Найпростіші варіанти повністю написані на простому С і мають менше 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
Ахмед Фасіх

Виявляється, ESSL IBM використовує варіацію алгоритму Страссена - ibm.com/support/knowledgecenter/en/SSFHY8/essl_welcome.html
ben-albrecht

2
більшість посилань мертві
Aurélien Pierre

PDF TSoPMC можна знайти на сторінці автора, за адресою cs.utexas.edu/users/rvdg/tmp/TSoPMC.pdf
Олексій Шпілкін

Хоча алгоритм Копперсміта-Винограда має гарну часову складність на папері, нотація Big O приховує дуже велику константу, тому вона починає ставати життєздатною лише для смішно великих матриць.
DiehardTheTryhard

26

Отже, в першу чергу BLAS - це лише інтерфейс з приблизно 50 функцій. Існує багато конкуруючих реалізацій інтерфейсу.

По-перше, я згадаю речі, які в значній мірі не пов'язані між собою:

  • Фортран проти С, не має ніякої різниці
  • Розширені матричні алгоритми, такі як Strassen, впровадження не використовують їх, оскільки вони не допомагають на практиці

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

Ці операції з невеликими розмірами фіксованого розміру (звані ядрами) жорстко кодуються в специфічному для процесора коді складання, використовуючи кілька функцій процесора їх цілі:

  • Інструкції у стилі SIMD
  • Паралелізм рівня навчання
  • Кеш-усвідомлення

Крім того, ці ядра можуть бути виконані паралельно відносно один одного за допомогою декількох потоків (CPU ядер) у типовому дизайні зменшення карти.

Погляньте на 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 за цикл)

По-третє, ваш код далеко не оптимальний:

  • Ви використовуєте необроблені покажчики, це означає, що компілятор повинен вважати, що вони можуть мати псевдонім. Є специфічні для компілятора ключові слова або прапори, які ви можете вказати, щоб сказати компілятору, що вони не псевдоніми. Крім того, ви повинні використовувати інші типи, ніж сировинні покажчики, які вирішують проблему.
  • Ви стискаєте кеш, виконуючи наївне проходження кожного рядка / стовпця вхідних матриць. Ви можете використовувати блокування, щоб виконати якомога більше роботи над меншим блоком матриці, який вписується в кеш процесора, перш ніж перейти до наступного блоку.
  • Для чисто чисельних завдань, Fortran є майже неперевершеним, і C ++ вимагає великої кількості угод, щоб досягти подібної швидкості. Це можна зробити, і є кілька бібліотек, що демонструють це (як правило, використовуючи шаблони виразів), але це не банально, і це не просто відбувається.

Дякую, я додав обмеження правильного коду згідно з пропозицією Джастікула, не побачив особливого покращення, мені подобається ідея після блоку. З цікавості, не знаючи розміру кешу CPU, як правильно підібрати оптимальний код?
DeusAduro

2
Ви цього не робите. Щоб отримати оптимальний код, потрібно знати розмір кешу CPU. Звичайно, недоліком цього є те, що ви ефективно жорстко кодуєте для найкращої роботи в одній родині процесорів.
jalf

2
Принаймні внутрішня петля тут уникає різких навантажень. Схоже, це написано для однієї матриці, яка вже переноситься. Ось чому це "лише" на порядок повільніше, ніж BLAS! Але так, це все ще молотить через відсутність блокування кеш-пам'яті. Ви впевнені, що Фортран багато допоможе? Я думаю, що все, що ви отримаєте тут, - це те, що restrict(не псевдонім) є типовим, на відміну від C / C ++ (І, на жаль, ISO C ++ не має restrictключового слова, тому вам доведеться використовувати __restrict__компілятори, які надають його як розширення).
Пітер Кордес

11

Я спеціально не знаю про реалізацію BLAS, але є більш ефективні алогіоритми для множення матриць, які мають складність, ніж O (n3). Добре відомим є алгоритм Страссена


8
Алгоритм Страссена не використовується в чисельниках з двох причин: 1) Він не стійкий. 2) Ви зберігаєте деякі обчислення, але це пов'язано з ціною, яку ви можете використовувати ієрархії кешу. На практиці ви навіть втрачаєте продуктивність.
Майкл Лен

4
Для практичної реалізації алгоритму Страссена, чітко побудованого на вихідному коді бібліотеки BLAS, існує нещодавня публікація: " Алгоритм Страссена перезавантажений " в SC16, який досягає більш високої продуктивності, ніж BLAS, навіть для розміру проблеми 1000x1000.
Цзянью Хуан

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 безпосередньо. Я думаю, що ця програма насправді не Fortran, а код асемблера (я не знаю, що є в бібліотеці, у мене немає джерел). Мені зовсім незрозуміло, чому cblas_dgemm не такий швидкий, оскільки, наскільки мені відомо, це просто обгортка для dgemm_.


3

Це реалістична швидкість. Для прикладу того, що можна зробити за допомогою асемблера SIMD над кодом C ++, див. Приклад функцій матриці iPhone - вони були в 8 разів швидшими, ніж версія C, і навіть не "оптимізована" збірка - ще немає прокладки труб і там це зайві операції стека.

Також ваш код не є " обмежити правильним " - як компілятор знає, що коли він змінює C, він не змінює A і B?


Звичайно, якщо ви назвали функцію типу 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
Вибачте, я не впевнений у VC, але з GCC потрібно активувати -fstrict-aliasing. Тут також є краще пояснення "обмежити": cellperformance.beyond3d.com/articles/2006/05/…
Justicle

2

Що стосується вихідного коду в ММ, множинні дані, пам'ять для більшості операцій, є основною причиною поганої роботи. Пам'ять працює в 100-1000 разів повільніше, ніж кеш.

Більшість прискорень відбувається завдяки використанню методів оптимізації циклу для цієї функції потрійного циклу в ММ множині. Використовуються дві основні методи оптимізації циклу; розгортання та блокування. Що стосується розгортання, ми розгортаємо дві зовнішні дві петлі та блокуємо її для повторного використання даних у кеші. Зовнішня розгортання циклу допомагає тимчасово оптимізувати доступ до даних, зменшуючи кількість посилань на пам'ять до одних і тих же даних у різний час протягом усієї операції. Блокування індексу циклу на конкретну кількість допомагає зберегти дані в кеші. Ви можете оптимізувати кеш L2 або кеш L3.

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


-24

З багатьох причин.

По-перше, компілятори Fortran дуже оптимізовані, і мова дозволяє їм бути такими. C і C ++ дуже вільні з точки зору обробки масиву (наприклад, у разі покажчиків, що стосуються однієї області пам'яті). Це означає, що компілятор не може знати заздалегідь, що робити, і змушений створити загальний код. У Fortran ваші випадки більш впорядковані, і компілятор краще контролює те, що відбувається, що дозволяє йому оптимізувати більше (наприклад, використовуючи регістри).

Інша справа, що 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 не записуються у фортран. Критичний до виконання код пишеться в зборі, а найпоширеніші в ці дні написані на С вище цього. Також BLAS визначає порядок рядків / стовпців як частину інтерфейсу, і реалізація може обробляти будь-яку комбінацію.
Андрій Томазос

10
Так, ця відповідь є абсолютно неправильною. На жаль, він сповнений здорового глузду, наприклад, заява BLAS була швидшою через Fortran. Мати 20 (!) Позитивних оцінок - це погано. Тепер ця чуттєвість навіть поширюється далі через популярність Stackoverflow!
Майкл Лен

12
Думаю, ви плутаєте неоптимізовану реалізацію посилань з виробничими реалізаціями. Реалізація посилань призначена лише для уточнення інтерфейсу та поведінки бібліотеки та була написана у Fortran з історичних причин. Це не для виробництва. У виробництві люди використовують оптимізовані реалізації, які демонструють таку саму поведінку, що і референтна реалізація. Я вивчив внутрішню програму ATLAS (яка підтримує Octave - Linux "MATLAB"), яку я можу підтвердити, що з першої руки написано на C / ASM. Практично, безумовно, також є комерційні реалізації.
Андрій Томазос

5
@KyleKanos: Так, ось джерело ATLAS: sourceforge.net/projects/math-atlas/files/Stable/3.10.1 Наскільки я знаю, це найпоширеніша портативна реалізація BLAS з відкритим кодом. Це написано на C / ASM. Високопродуктивні виробники процесорів, такі як Intel, також забезпечують реалізацію BLAS, особливо оптимізовану для своїх чіпів. Я гарантую, що на низькому рівні частини бібліотеки Intels написані в (duuh) x86 збірці, і я впевнений, що частини середнього рівня будуть написані на C або C ++.
Андрій Томазос

9
@KyleKanos: Ви розгублені. Netlib BLAS є базовою реалізацією. Реалізація посилань набагато повільніше, ніж оптимізовані реалізації (див. Порівняння продуктивності ). Коли хтось каже, що вони використовують кластер BLAS Netlib на кластері, це не означає, що він фактично використовує реалізацію посилання netlib. Це було б просто нерозумно. Це просто означає, що вони використовують lib з тим же інтерфейсом, що і netlib blas.
Андрій Томазос
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.