Оптимізація нейромережевої платформи Caffe для архітектури Intel

Сучасні програми, які претендують на звання ефективних, повинні враховувати особливості апаратного забезпечення, на якому вони будуть виконуватися. Зокрема, мова йде про багатоядерних процесорах, наприклад, таких, як Intel Xeon та Intel Xeon Phi, про великих розмірах кеш-пам'яті, про набори інструкцій, скажімо, Intel AVX2 і Intel AVX-512, які дозволяють підвищити продуктивність обчислень.


Ледве стрималися, щоб не пожартувати про руссиано)

Ось, наприклад, Caffe – популярна платформа для розробки нейронних мереж глибокого навчання. Її створили в Berkley Vision and Learning Center (BVLC), вона припала до душі спільноти незалежних розробників, які вносять посильний внесок в її розвиток. Платформа живе і розвивається, тому доказ – статистика на сторінці проекту GitHub. Caffe називають «швидкої відкритою платформою для глибокого навчання». Чи можна прискорити ось такий «швидкий» набір інструментів? Задавшись цим питанням, ми вирішили оптимізувати Caffe для архітектури Intel.

Забігаючи вперед, відзначимо, що Caffe, завдяки інтеграції з Intel Math Kernel Library 2017 і набору оптимізацій, які ми виконали, дотримуючись плану, викладеному в це матеріалі, стала працювати на процесорах Intel більш ніж в 10 разів швидше базової версії, яку ми в подальшому будемо називати BVLC Caffe. Версію, оптимізовану для архітектури Intel, далі, для стислості будемо називати Intel Caffe. Ось її вихідний код.

Основні напрями покращення продуктивності, подробиці про які читайте нижче, полягали в рефакторинге коду, його оптимізації в розрахунку на використання наборів векторних інструкцій, таких як Intel AVX2, в тонкій настройці компіляції, в підвищенні ефективності багатопоточного виконання коду з використанням OpenMP. Випробування проводилися на системі з двома процесорами Intel Xeon. Зокрема, ми досліджували швидкість нейронної мережі, побудованої засобами Caffe, при роботі із зображеннями з набору CIFAR-10. Результати виконання програми аналізували в Intel VTune Amplifier XE 2017 й з допомогою інших інструментів.

Схожий підхід можна використовувати для поліпшення продуктивності різних програм, наприклад, інших платформ для глибокого навчання нейронних мереж.

Перш ніж переходити до питань оптимізації, розповімо про алгоритми глибокого навчання та про завдання, які вирішують за їх допомогою.

Про алгоритми глибокого навчання
Алгоритми глибокого навчання – це частина більш загального класу алгоритмів машинного навчання, які в останні роки показали значні результати в розпізнаванні образів на фото і відео, розпізнавання мовлення, в обробці природної мови, і в інших областях, де доводиться мати справу з величезними обсягами інформації та вирішувати задачі аналізу даних. Успіх глибокого навчання базується на останніх досягненнях в області обчислень і алгоритмів, в можливості обробляти великі набори даних. Принцип роботи таких алгоритмів полягає в тому, що дані пропускають через шари мережі, в яких здійснюється перетворення інформації, витяг з неї все більш складних ознак.

Ось приклад того, як кожен рівень глибокої нейронної мережі навчений ідентифікувати ознаки все більш високої складності. Тут показаний невеликий набір ознак, розпізнаваних глибокої мережею, візуалізований у вигляді зображень у відтінках сірого. Тут же показані вихідні кольорові зображення, обробка яких веде до виділення цих ознак. Зображення взято звідси.


Сверточная нейронна мережа

Для роботи алгоритмів глибокого навчання з учителем потрібно розмічений набір даних. Три популярних типу глибоких нейронних мереж, які навчають з учителем, це багатошаровий перцептрон (Multilayer Perceptron, MLM), згорткові нейронні мережі (Convolution Neural Network, CNN), та рекурентні нейронні мережі (Recurrent Neural Network, RNN). У цих мережах вхідні дані, при проходженні їх через кожен шар мережі, піддають серій лінійних і нелінійних перетворень. У результаті формуються вихідні дані мережі. Відповідь мережі порівнюють з очікуваним результатом, знаходять помилки, а потім, для вихідного шару, обчислюють вектор градієнта поверхні помилок, з'ясовують, який внесок у відповідь мережі вносять синаптичні ваги нейронів, з урахуванням активаційних функцій, після чого виконують таку ж процедуру для інших верств, застосовуючи раніше отримані дані. Цей метод навчання називають алгоритмом зворотного поширення помилки, в результаті його застосування здійснюється покрокова модифікація вагових коефіцієнтів нейронів мережі.

У багатошарових перцептронах вхідні дані у кожному шарі (представленому вектором) спочатку множать на повністю заповнену матрицю вагових коефіцієнтів, унікальну для шару. У рекурентних мережах така матриця (або матриці) одна і та ж для кожного шару (так як шар рекурентний), і властивості мережі залежать від вхідного сигналу. Згорткові мережі схожі на багатошарові перцептрони, але вони використовують розріджені матриці для прихованих шарів, званих згортковими. В таких мережах множення матриць представлено згорткою матричного представлення ваг з матричним представленням вхідних даних шару. Згорткові мережі популярні в розпізнаванні зображень, але вони знаходять застосування і в розпізнаванні мови, і в обробці природних мов. Тут можна почитати про таких мережах докладніше.

Caffe, CIFAR-10 і класифікація зображень
Як вже було сказано, тут ми збираємося оптимізувати для архітектури Intel BVLC Caffe – популярну платформу для створення і дослідження мереж глибокого навчання. Відчувати вихідну і оптимізовану версію платформи будемо, використовуючи набір даних CIFAR-10, який часто застосовують у задачах класифікації зображень, модель нейронної мережі, побудовану в Caffe.


Приклад зображень з набору CIFAR-10

Набір даних CIFAR-10 складається з 60000 кольорових зображень розміром 32x32 пікселя, розділених на 10 класів: літак, автомобіль, птиця, кішка, олень, собака, жаба, кінь, корабель і вантажівка. Класи не перетинаються. Наприклад, тут немає перекриття між класами «автомобіль» і «вантажівка». До «автомобілів» відносяться, наприклад, седани і позашляховики. В клас «вантажівка» входять тільки важкі вантажівки, а, наприклад, вантажівок-пікапів немає ні в одній з груп зображень.

Мережа, яка використовується в ході тестування продуктивності, що містить шари різних типів. Зокрема, це шари з сигмоидной функцією активації (такі шари, в термінології Caffe, мають тип Sigmoid), згорткові шари (тип Convolution), шари просторового об'єднання або, як їх ще називають, шари підвибірки (тип Pooling), шари пакетної нормалізації (тип BatchNorm), полносвязные шари (тип InnerProduct). На виході мережі знаходиться шар з активаційний функцією Softmax (тип SoftmaxWithLoss). Докладніше про цю мережі та її шарах ми поговоримо нижче. А зараз приступимо до аналізу вихідної версії Caffe.

Початковий аналіз продуктивності
Один з методів оцінки продуктивності BVLC Caffe і Intel Caffe полягає у використанні команди time, яка обчислює час, необхідний для проходження сигналу по шарах в прямому і зворотному напрямку. Ця команда дуже корисна для виміру часу, що витрачається на обчислення в кожному рівні, і для отримання порівняльного часу виконання для різних моделей:

./build/tools/caffe time \
--model=examples/cifar10/cifar10_full_sigmoid_train_test_bn.prototxt \
-iterations 1000

В даному випадку «ітерацією» (тим, що задає параметр iteration) називається один прямий і зворотний прохід по пакету зображень. Вищенаведена команда виводить середнє час виконання для 1000 ітерацій, як для окремих верств населення, так і для всієї мережі. Ось результати роботи цієї команди BVLC Caffe.


Вивід команди time для BVLC Caffe

У тестах ми використовували систему з двома сокетами. У кожному був встановлений процесор Intel Xeon E5-2699 v3 (2.3 ГГц) з 18-ма фізичними ядрами. При цьому технологія Intel Hyper-Threading була відключена. У системі, таким чином, було всього 36 фізичних процесорних ядер і таке ж число потоків OpenMP, що було встановлено за допомогою змінної середовища OMP_NUM_THREADS. Якщо не вказано інше, в наших експериментах використовувалася саме така конфігурація. Зверніть увагу на те, що ми рекомендуємо дозволити Intel Caffe автоматично налаштовувати змінні середовища OpenMP, замість того, щоб ставити їх самостійно. У системі, крім того, встановлено 64 Гб DDR4-пам'яті, що працює на частоті 2.133 МГц.

Тут показані результати тестування продуктивності, яких вдалося досягти завдяки оптимізації коду інженерами Intel. Для вимірювання продуктивності ми використовували такі інструменти:

  • Callgrind з набору інструментів Valgrind.
  • Intel VTune Amplifier XE 2017 beta.
Кошти з Intel VTune Amplifier XE надають такі відомості:

  • Функції, що створюють найбільшу навантаження на систему (hotspots).
  • Системні виклики (в тому числі – переключення задач).
  • Використання процесора і кеш-пам'яті.
  • Розподіл навантаження по потокам OpenMP.
  • Блокування потоків.
  • Використання пам'яті.
Аналізи продуктивності можна використовувати для пошуку підходящих кандидатів на оптимізацію, таких, як функції, що створюють велике навантаження на систему, і виклики функцій, які виконуються порівняно довго.

На малюнку нижче показано узагальнені дані аналізу продуктивності BVLC Caffe з Intel VTune, отримані після 100 ітерацій. Показник Elapsed Time, розташований у верхній частині малюнка, становить 37 секунд. Це – час, який знадобився для виконання коду на тестовій системі. Показник CPU Time, процесорний час, становить 1306 секунд. Це трохи менше, ніж 37 секунд, помножені на 36 ядер (1332 секунди). Даний показник являє собою загальну тривалість виконання коду у всіх потоках (або на всіх ядрах, так як в нашому випадку технологія Intel HT була відключена), які використовуються в обчисленнях.


Загальні результати аналізу виконання BVLC Caffe на наборі даних CIFAR-10 в Intel VTune Amplifier XE 2017 beta

Гістограма використання процесора, яка знаходиться в нижній частині малюнка, що вказує на те, як часто в ході тесту певну кількість потоків задіюється одночасно. В даному випадку, з 37 секунд, 14 припадає на один потік (тобто – на одне ядро). Весь інший час ми бачимо дуже неефективну багатопотокову обробку, при цьому, в основному, в роботі беруть участь не менш 20 потоків.

Розділ Top Hotspots, розташований в середині малюнка, що вказує на те, які функції припадає найбільше роботи. Тут перераховані виклики функцій і внесок кожної з них у загальний час роботи процесора. Функція kmp_fork_barrier – це зовнішня OpenMP-функція, на виконання коду якої йде 1130 секунд процесорного часу. Це означає, що близько 87% робочого часу процесора йде на те, що потоки діють в цій бар'єрної функції, не роблячи нічого корисного.

У вихідному коді BVLC Caffe є рядок #pragma omp parallel. Проте в самому коді не спостерігається явного використання бібліотеки OpenMP для організації многопоточной обробки даних. При цьому всередині Intel MKL потоки OpenMP використовуються для розпаралелювання виконання деяких базових математичних розрахунків. Для того, щоб підтвердити це розпаралелювання, ми можемо скористатися вкладкою Bottom-up в Intel VTune XE, вміст якої, після тестування BVLC Caffe на наборі даних CIFAR-10, наведено на малюнку нижче. Тут можна знайти перелік викликів функцій і додаткові відомості про них. Зокрема, нас цікавлять показники Effective Time by Utilization (верхня частина вкладки) та показники розподілу навантаження, створюваної функціями, по потокам (нижня частина).


Візуалізація часових параметрів виконання функцій та перелік функцій, найсильніше навантажують систему при виконанні BVLC Caffe на наборі даних CIFAR-10

Функція gemm_omp_driver_v2 – це частина бібліотеки libmkl_intel_thread.so – узагальнена реалізація множення матриць (GEMM) з Intel MKL. У внутрішніх механізмах цієї функції задіяна OpenMP-багатопоточність. Функція множення матриць з Intel MKL – це основна функція використовується в процедурах прямого і зворотного поширення, тобто, в операціях отримання відповіді мережі і її навчання. Intel MKL використовує багатопотокове виконання, що зазвичай зменшує час виконання GEMM-обчислень. Однак, в даному конкретному випадку операція згортки для зображень розміром 32x32 створює не надто велике навантаження на систему, що не дозволяє ефективно використовувати всі 36 OpenMP-потоку на 36 ядрах в одній GEMM-операції. Тому, як буде показано нижче, потрібно використання різних схем багатопоточності і паралелізації виконання коду.

Для того, щоб продемонструвати додаткове навантаження на систему, яку створює необхідність працювати з безліччю потоків OpenMP, ми запустили той же код з змінної оточення OMP_NUM_THREADS=1, а потім порівняли час виконання з попереднім результатом. Те, що у нас вийшло, представлено на малюнку нижче. Тут ми бачимо показник Elapsed Time, рівний 31.1 секунді, замість 37 секунд з попереднього випробування. Записавши в змінну оточення одиницю, ми примусили OpenMP до створення тільки одного потоку і використання його для виконання коду. Отримана різниця в майже шість секунд вказує на додаткове навантаження на систему, яку викликають операції ініціалізації і синхронізації потоків OpenMP.


Загальні результати аналізу виконання BVLC Caffe на наборі даних CIFAR-10 в Intel VTune Amplifier XE 2017 beta при використанні одного потоку

У центральній частині вищенаведеного рисунка є список функцій, найбільш сильно навантажують систему. Серед них ми виявили три основних кандидати на оптимізацію. А саме – це функції im2col_cpu, col2im_cpu та PoolingLayer::Forward_cpu.

Оптимізація коду
Робота з набором даних CIFAR-10 с в середовищі Caffe, оптимізованої для архітектури Intel, приблизно 13.5 разів швидше, ніж при використанні BVLC Caffe. На малюнку нижче представлені середні результати після 1000 ітерацій. Зліва наведено дані BVLC Caffe, праворуч – Intel Caffe. Видно, що в першому випадку загальний час виконання склало 270 мс., а в другому – 20 мс.


Порівняння продуктивності BVLC Caffe і Intel Caffe

Подробиці про те, як задавати параметри обчислень для шарів, можна знайти на тут.
У наступному розділі будуть описані оптимізації, використані для поліпшення продуктивності розрахунків, що застосовуються в різних шарах. Ми слідували методичним посібникам з програми Intel Modern Code. Деякі з оптимізації засновані на базових математичних функціях з Intel MKL 2017.

Скалярна і послідовна оптимізація
▍Векторизація коду
Після профілювання коду BVLC Caffe і виявлення найбільш навантажених функцій, які споживають найбільше процесорного часу, ми почали роботу над векторизацией коду. Серед внесених змін були наступні:

  • Поліпшення роботи з бібліотеками Basic Linear Algebra Subprograms (BLAS), а саме – перехід з Automatically Tuned Linear Algebra System (ATLAS) на Intel MKL.
  • Оптимізації в процесі складання коду (використання JIT-асемблера Xbyak).
  • Векторизація коду з використанням GNU Compiler Collection (GCC) та OpenMP.
У BVLC Caffe є можливість використання викликів функції Intel MKL BLAS або інших реалізацій тих же механізмів. Наприклад, функція GEMM оптимізована в розрахунку на векторизацію, багатопоточне виконання та ефективне використання кеш-пам'яті. Для поліпшення векторизації ми так само використовували Xbyak – JIT-асемблер для архітектур x86 (IA-32) і x64 (AMD64 або x86-64). Xbyak підтримує наступні набори векторних інструкцій: MMX, Intel Streaming SIMD Extensions (Intel SSE), Intel SSE2, Intel SSE3, Intel SSE4, модуль обчислень з плаваючою комою, Intel AVX, Intel AVX2 і Intel AVX-512.

Xbyak – це x86/x64-асемблер для C++, бібліотека, спеціально створена для підвищення ефективності виконання коду. Xbyak надається у вигляді заголовкого файлу. Він може динамічно збирати мнемонічні інструкції для архітектур x86 і x64. JIT-генерація двійкового коду в процесі виконання дає додаткові можливості оптимізації. Наприклад, це оптимізація квантування, операції поелементного поділу одного масиву на інший, або оптимізація поліноміальних обчислень завдяки автоматичному створенню потрібних функцій під час виконання програми. Завдяки підтримці наборів векторних інструкцій Intel AVX і Intel AVX2, з допомогою Xbyak можна досягти кращого рівня векторизації коду в Caffe, оптимізованому для архітектури Intel. У самої свіжої версії Xbyak є підтримка набору векторних інструкцій Intel AVX-512. Це дозволяє поліпшити продуктивність обчислень на процесорах Intel Xeon Phi сімейства x200.

Поліпшення показників векторизації дає можливість Xbyak, з допомогою SIMD-інструкцій, обробляти більше даних одночасно, що дозволяє більш ефективно задіяти паралельну обробку даних. Ми використовували Xbyak при оптимізації коду, що значно поліпшило продуктивність розрахунків в шарах просторового об'єднання. Якщо відомі параметри просторового об'єднання, можна згенерувати асемблерний код для конкретних моделей об'єднання, в яких використовується певне вікно обробки даних або алгоритм. В результаті виходить цілком звичайна з вигляду збірка, яка, що доведено, працює ефективніше, ніж код на C++, скомпільований без використання Xbyak.

▍Загальні оптимізації коду
Інші послідовні оптимізації включали в себе наступне:

  • Зменшення складності алгоритмів.
  • Зменшення обсягу обчислень.
  • Розгортання циклів.
Позбавлення від багаторазового виконання коду, результати якого не змінюються – це одна з технік скалярної оптимізації, яку ми застосували. Це було зроблено для того, щоб заздалегідь обчислити те, що інакше б вираховувалось всередині циклу з максимальною глибиною вкладеності.
Розглянемо, наприклад, такий фрагмент коду:

for (int h_col = 0; h_col < height_col; ++h_col) {
for (int w_col = 0; w_col < width_col; ++w_col) {
int h_im = h_col * stride_h - pad_h + h_offset;
int w_im = w_col * stride_w - pad_w + w_offset;

У третьому рядку цього фрагмента, для обчислення змінної h_im, не використовується індекс внутрішнього циклу w_col. Але, незважаючи на це, обчислення даної змінної проводиться в кожній ітерації вкладеного циклу. Як варіант, ми можемо перемістити цю сходинку за межі внутрішнього циклу, привівши код до такого виду:

for (int h_col = 0; h_col < height_col; ++h_col) {
int h_im = h_col * stride_h - pad_h + h_offset;
for (int w_col = 0; w_col < width_col; ++w_col) {
int w_im = w_col * stride_w - pad_w + w_offset;

Оптимізації, специфічні для процесора, системи та інші загальні підходи до покращення коду
Ось які додаткові загальні оптимізації коду були застосовані:

  • Покращення реалізації функцій im2col_cpu та col2im_cpu.
  • Зменшення складності операції пакетної нормалізації.
  • Оптимізації, специфічні для процесора і системи.
  • Використання одного ядра на обчислювальний потік.
  • Усунення переміщення потоків між обчислювальними ядрами.
Intel VTune Amplifier XE з'ясував, що функція im2col_cpu – одна з найбільш сильно навантажують систему. Це означає, що вона – хороший кандидат на оптимізацію продуктивності. Функція im2col_cpu – це реалізація стандартного кроку в операції прямої згортки. Кожен локальний фрагмент розгортається в окремий вектор, всі зображення конвертується в більш велику матрицю (що підвищує інтенсивність роботи з пам'яттю), рядки якої відповідають безлічі місць, де були застосовані фільтри.

Одна з технік для оптимізації функції im2col_cpu полягає в скороченні числа операцій, необхідних для доступу до даних. У коді BVLC Caffe є три вкладених циклу, в яких виконується прохід по пікселях зображення:

for (int c_col = 0; c_col < channels_col; ++c_col)
for (int h_col = 0; h_col < height_col; ++h_col)
for (int w_col = 0; w_col < width_col; ++w_col)
data_col[(c_col*height_col+h_col)*width_col+w_col] = // ...

У цьому фрагменті коду BVLC Caffe спочатку обчислював відповідні індекси елементів масиву data_col, хоча індекси цього масиву просто обробляються послідовно. Таким чином, чотири арифметичні операції (додавання і два множення) можна замінити однією операцією инкрементации індексу. Крім того, складність перевірки умови можна зменшити виходячи з наступного:

/* Функція використовує приведення типу int до unsigned для перевірки 
того, чи є значення параметра a більшим або рівним нулю, 
і меншим, ніж значення параметра b. Тип параметра b – unsigned, 
він завжди позитивний, таким чином, його значення завжди менше, 
чим 0x800..., при цьому перетворення типу параметра з негативним 
значенням завжди приводить його до числа, яке більше, ніж 0x800... 
Приведення типів дозволяє використовувати одну умову замість двох. */
inline bool is_a_ge_zero_and_a_lt_b(int a, int b) {
return static_cast<unsigned>(a) < static_cast<unsigned>(b);
}

У коді BVLC Caffe була перевірка умови виду if (x > = 0 && x < N), x та N – цілі числа зі знаком, при цьому N – завжди додатне число. Перетворення цих цілих чисел до цілих чисел без знаку дозволяє змінити інтервал порівняння. Замість того, щоб виконувати дві операції порівняння та обчислення логічного І, після перетворення типу досить одного порівняння:

if (((unsigned) x) < ((unsigned) N))

Для того, щоб уникнути переміщення потоків операційною системою між обчислювальними ядрами, ми використовували змінну середовища OpenMP: KMP_AFFINITY = compact, granularity = fine. Компактне розташування сусідніх потоків може поліпшити продуктивність операцій GEMM, так як потоки, які спільно працюють з одним і тим же кешем останнього рівня (last-level cache, LLC), можуть повторно використовувати дані, раніше записані рядка кеша.

Ось матеріал, в якому можна знайти подробиці про оптимізацію, пов'язаної з блокуванням кешу, про особливості оптимальної компоновки даних і векторизації.

Паралелізація коду з використанням OpenMP
▍Шари нейронної мережі
В ході застосування OpenMP-паралелізації були оптимізовані наступні механізми нейронної мережі:

  • Шар згортки (Convolution).
  • Шар зворотного перетворення згортки (Deconvolution).
  • Шар локальної нормалізації (Local response normalization, LRN).
  • Шар з полулинейной функцією активації (Rectified-Linear Unit, ReLU)
  • Шар з функцією активації Softmax.
  • Шар об'єднання (Concatenation).
  • Утиліти для OpenBLAS-оптимізації, такі як операція vPowx — y[i] = x[i]β, операції caffe_set, caffe_copy та caffe_rng_bernoulli.
  • Шар просторового об'єднання, або підвибірки (Pooling).
  • Шар «проріджування» мережі для запобігання ефекту перенавчання (Dropout).
  • Шар пакетної нормалізації (Batch normalization).
  • Шар даних (Data).
  • Шар для виконання поэлементных операцій (Eltwise).
▍Шар згортки
Шар згортки, що цілком відповідає його назві, виконує згортку вхідних даних, використовуючи набір модифікованих в ході навчання мережі ваг, або фільтрів, кожний з яких дозволяє отримати одну карту ознак у вихідному зображенні. Ця оптимізація запобігає недостатнє використання апаратних ресурсів для одного набору вхідних карт ознак.

template < typename Dtype>
void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& \
bottom, const vector<Blob<Dtype>*>& top) {
const Dtype* weight = this->blobs_[0]->cpu_data();

// Якщо є більше доступних потоків, ніж пакетів для обробки, значить
// ми даремно використовуємо ресурси (менше пакетів, ніж 36 
// на нашій тестовій системі).
// Повідомимо про це MKL.
for (int i = 0; i < bottom.size(); ++i) {
const Dtype* bottom_data = bottom[i]->cpu_data();
Dtype* top_data = top[i]->mutable_cpu_data();
#ifdef _OPENMP
#pragma omp parallel for num_threads(this->num_of_threads_)
#endif
for (int n = 0; n < this->num_; ++n) {
this->forward_cpu_gemm(bottom_data + n*this->bottom_dim_,
weight,
top_data + n*this->top_dim_);
if (this->bias_term_) {
const Dtype* bias = this->blobs_[1]->cpu_data();
this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
}
}
}
}

Ми обробляємо k = min(num_threads,batch_size) наборів карт input_feature. Наприклад, k операцій im2col відбуваються паралельно і виконується k звернень до Intel MKL. Intel MKL перемикається в однопотоковий режим виконання автоматично і загальна продуктивність виявляється краще, ніж раніше, коли Intel MKL обробляла один пакет. Така поведінка задано у файлі з вихідним кодом src/caffe/layers/base_conv_layer.cpp. Це реалізація оптимізованої многопоточной обробки з використанням OpenMP з файлу з початковим кодом src/caffe/layers/conv_layer.cpp.

▍Шар підвибірки
Max-pooling, average-pooling, і stochastic-pooling (ще не реалізований) – це різні методи понижувальної дискретизації, при цьому max-pooling – найпопулярніший метод. Шар підвибірки розбиває результат, отриманий від попереднього шару, на набір зазвичай не перекриваються прямокутних фрагментів. Для кожного такого фрагмента шар потім виводить максимум (max-pooling), середнє арифметичне (average-pooling), або (в майбутньому) стохастичне значення (stochastic-pooling), отримане з мультиномиального розподілу, сформованого з функцій активації кожного фрагмента.

Шари підвибірки корисні в згорткових мережах з трьох основних причин:

  • Подвыборка зменшує розмірність задачі і обчислювальне навантаження на вищерозміщені шари.
  • Подвыборка для нижчих шарів дозволяє ядер згортки в шарах, розташованих вище, покривати великі області вхідних даних, і, таким чином, навчатися більш складним ознаками. Наприклад, ядро з шару, розташованого нижче, зазвичай навчається розпізнавати невеликі елементи зображення, в той час як ядро шару, розташованого вище, може навчатися розпізнавати більш складних структур, таких, як зображення лісів або пляжів.
  • Метод max-pooling підвищує стійкість мережі до зсуву зображення. З восьми можливих напрямів у яких фрагмент 2x2 (звичайний розмір вікна підвибірки) може бути зрушений на один піксель, три дадуть те ж саме максимальне значення. Для вікна 3x3 вже п'ять напрямів дадуть те ж саме максимальне значення.
Подвыборка працює на жодній карті ознак, тому ми використовували Xbyak для того, щоб виконати ефективну процедуру складання, яка допоможе створити потрібну нам вибірку для однієї або більшої кількості вхідних карт ознак. Ця методика може бути реалізована для пакету вхідних карт ознак, коли процедура виконується паралельно в OpenMP.

Обчислення шару підвибірки виконуються паралельно, з використанням OpenMP-багатопоточності. Це можливо завдяки тому, що зображення незалежні:

#ifdef _OPENMP
#pragma omp parallel for collapse(2)
#endif
for (int image = 0; image < num_batches; ++image)
for (int channel = 0; channel < num_channels; ++channel)
generator_func(bottom_data, top_data, top_count, image, image+1,
mask, channel, channel+1, this, use_top_mask);
}

Завдяки висловом collapse(2), директива OpenMP #pragma omp parallel поширюється на обидва вкладених циклу for, які виконують прохід по зображеннях в пакеті і каналах зображень, комбінуючи цикли в один і паралельно виконуючи те, що вийшло.

▍Шар Softmax і функція втрат
Функція втрат – це ключовий компонент в машинному навчанні. Саме ця функція використовується при порівнянні виходу мережі з цільовим показником, для пошуку помилки. Після цього проводиться настроювання вагових коефіцієнтів мережі для зменшення значення цієї функції, тобто, для зменшення помилки, відхилення того, що видає мережа, від бажаного виходу. В нашій моделі в якості функції втрат використовується softmax (тип шару – SoftmaxWithLoss).

Таку функцію активації використовують у тому разі, коли виходи мережі символізують ймовірність певних подій, або, як у нашому випадку, ймовірність приналежності зображень до різних класів. Зокрема, в мультиномиальной логістичної регресії (проблема класифікації на безліч класів), вхідні дані для цієї функції – результат K різних лінійних функцій, і передбачена ймовірність j-того класу для вектора x обчислюється за такою формулою:


При багатопотоковому виконання цих обчислень застосовується підхід з використанням головного і підлеглого потоків. Так, головний потік запускає якесь число підлеглих, розподіляючи завдання між ними. Підлеглі потоки потім виконуються паралельно, так як вони призначаються різних ядер.

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

// поділ
#ifdef _OPENMP
#pragma omp parallel for
#endif
for (int j = 0; j < channels; j++) {
caffe_div(inner_num_, top_data + j*inner_num_, scale_data,
top_data + j*inner_num_);
}

▍ReLU і сигмоїдальна функції активації в повноз'вязних шарах
ReLU – це найпопулярніший на сьогодні клас нелінійних функцій, які використовуються в алгоритмах глибокого навчання. Полносвязные шари – це поелементні оператори, які беруть двійковий об'єкт (blob в термінології Caffe), що видається нижележащим шаром, і подають на вищерозміщений шар перетворений набір даних того ж розміру. (Такий набір даних – це звичайний масив, який представляє уніфікований інтерфейс платформи Caffe. Коли дані і знайдені помилки поширюються по мережі, Caffe працює з інформацією у вигляді таких об'єктів).

Шар з активаційний функцією ReLU бере вхідна значення x і подає на вихід те ж саме x, якщо він більше нуля, від'ємні значення перемножує на параметр negative_slope за такою формулою:


За замовчуванням значення параметра negative_slope дорівнює нулю, що еквівалентно стандартної функції ReLU, яка повертає максимальне значення після порівняння переданого їй значення з нулем: max(x, 0). Через незалежності процесу активації даних, кожен набір даних може бути оброблений паралельно:

template < typename Dtype>
void ReLULayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
const Dtype* bottom_data = bottom[0]->cpu_data();
Dtype* top_data = top[0]->mutable_cpu_data();
const int count = bottom[0]->count();
Dtype negative_slope=this->layer_param_.relu_param().negative_slope();
#ifdef _OPENMP
#pragma omp parallel for
#endif
for (int i = 0; i < count; ++i) {
top_data[i] = std::max(bottom_data[i], Dtype(0))
+ negative_slope * std::min(bottom_data[i], Dtype(0));
}
}

Схожі паралельні обчислення можна використовувати і процедури зворотного поширення помилки:

template < typename Dtype>
void ReLULayer<Dtype>::Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down,
const vector<Blob<Dtype>*>& bottom){
if (propagate_down[0]) {
const Dtype* bottom_data = bottom[0]->cpu_data();
const Dtype* top_diff = top[0]->cpu_diff();
Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
const int count = bottom[0]->count();
Dtype negative_slope=this->layer_param_.relu_param().negative_slope();
#ifdef _OPENMP
#pragma omp parallel for
#endif
for (int i = 0; i < count; ++i) {
bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0)
+ negative_slope * (bottom_data[i] < = 0));
}
}
}

Таким же чином можна розпаралелити обчислення сигмоидной функції S(x) = 1 / (1 + exp(-x)):

#ifdef _OPENMP
#pragma omp parallel for
#endif
for (int i = 0; i < count; ++i) {
top_data[i] = sigmoid(bottom_data[i]);
}

Так як MKL не надає базових математичних операцій для реалізації ReLU-функцій, для того, щоб додати цей функціонал в систему, ми спробували реалізувати оптимізовану версію ReLU-шару на асемблері (з використанням Xbyak). Однак, після випробувань, ми не виявили помітного зростання продуктивності на процесорах Intel Xeon. Можливо це так із-за обмеженої пропускної здатності пам'яті. Паралелізація існуючого коду на C++ виявилася досить хорошою для поліпшення загальної продуктивності.

Висновки
У попередньому розділі ми розглянули різні компоненти і шари нейронних мереж, і те, як дані, що обробляються в цих шарах, розподіляються за потоками OpenMP і Intel MKL. Гістограма використання процесора, наведена нижче, показує, наскільки часто якесь число потоків виконується паралельно після оптимізації коду.


Загальні результати аналізу виконання реалізації Caffe, оптимізованої для архітектури Intel, на завданні CIFAR-10 в Intel VTune Amplifier XE 2017 beta

З використанням Caffe, оптимізованого для архітектури Intel, кількість одночасно виконуваних потоків значно зросла. Час виконання на нашій тестовій системі впало з 37 секунд для неоптимизированного коду BVLC Caffe, до всього 3.6 секунд для оптимізованої версії. Загальна продуктивність зросла більш ніж у 10 разів.

Ка показано в розділі Elapsed Time, у верхній частині малюнка, частина часу виконання відноситься до показника Spin Time, що вказує на час, що витрачається на очікування, а не на корисну роботу. В результаті продуктивність не росте лінійно при збільшенні числа потоків (згідно із законом Амдала). Крім того, тут все ще є ділянки, виконуються послідовно, не параллелизованные з використанням OpenMP. Повторна ініціалізація паралельних ділянок OpenMP була значно оптимізована для останніх реалізацій бібліотеки OpenMP, але вона все ще створює досить помітну додаткове навантаження на систему. Переміщення паралельних ділянок в головну функцію може, в потенціалі, поліпшити продуктивність навіть більше, але це потребуватиме значного рефакторінгу коду.

На малюнку нижче узагальнені описані техніки оптимізації та принципи переробки коду, яким ми йшли, займаючись оптимізацією Caffe для архітектури Intel.


Покроковий підхід програми Intel Modern Code

В ході випробувань ми користувалися Intel VTune Amplifier XE 2017 beta для пошуку ділянок коду, які створюють найбільшу навантаження на систему, і оптимізація яких здатна принести помітний виграш в продуктивності. Ми реалізували скалярні і послідовні оптимізації, включаючи усунення коду, результати виконання якого виявляються одними і тими ж при багаторазовому виклик. Так само ми провели скорочення або спрощення арифметичних операцій, оптимізувавши цикли і перевірки умов. Далі ми поліпшили код в розрахунку на його векторизацію, слідуючи загальним принципам, викладеним у матеріалі про автоматичної векторизації в GCC. Застосування JIT-асемблера Xbyak дозволило нам більш ефективно задіяти SIMD-інструкції.

Ми реалізували багатопоточність для обчислень, проведених усередині шарів нейронної мережі за допомогою бібліотеки OpenMP, там, де операції над зображеннями або каналами були незалежні від даних. Останній крок у застосуванні підходу програми Intel Modern Code включав масштабування додатку, яке початково здійснювалося на одному вузлі, на многоядерную архітектуру, і на кластерну середовище з безліччю вузлів. Це, на даний момент, основний предмет нашого дослідження. Крім того, ми застосували оптимізації, орієнтовані на повторне використання кеш-пам'яті, що дозволило поліпшити продуктивність обчислень. Подробиці про подібних оптимізацію можна знайти на тут. Оптимізація коду для процесорів Intel Xeon Phi сімейства x200 включала в себе використання пам'яті MCDRAM з високою пропускною здатністю і роботу в режимі NUMA.

Оптимізація Caffe для архітектури Intel не тільки значно поліпшила продуктивність обчислень, але й дозволила отримувати набагато більш складні набори ознак з графічних даних. Якщо ви використовуєте у власних нейромережевих дослідженнях Caffe, запускаючи код на системах із процесорами Intel, оптимізована версія цієї платформи значно розширить ваші можливості.

Крім того, сподіваємося, що наша розповідь про поліпшення коду, про інструменти для аналізу швидкості роботи і оптимізації програм, допоможе вам у справі поліпшення продуктивності ваших додатків, як належать до сфери глибокого навчання нейронних мереж, так і будь-яких інших.
Intel хотіла б висловити подяку Борису Гінзбургу за його ідеї і первісний внесок в розробку многопоточной OpenMP-версії Nero, оптимізованої для архітектури Intel.
Подробиці про програму Intel Modern Code можна почитати тут і тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.