Как да обучите много голям и дълбок модел на един графичен процесор?

Проблем: Ограничаване на GPU паметта

Вярвам, че не е нужно да обяснявам колко мощен GPU може да бъде вече за трениране на дълбоки невронни мрежи. Използвайки често популярна рамка за ML, е много по-удобно да присвоите изчисленията на GPU (и), отколкото да правите всичко от нулата. Има обаче едно нещо, което може да създаде сценарии за кошмар - границите на DRAM на вашия GPU (и).

Въпреки това, като се има предвид размерът на вашия модел и големината на вашите партиди, всъщност можете да изчислите колко GPU памет се нуждаете за обучение, без всъщност да я изпълнявате. Например, за обучение на AlexNet с размер на партидата от 128 са нужни 1,1 GB глобална памет, а това е само 5 конволюционни слоя плюс 2 напълно свързани слоя. Ако разгледаме по-голям модел, да речем VGG-16, използването на партиден размер 128 ще изисква около 14 GB глобална памет. Настоящото състояние на NVIDIA Titan X има капацитет на паметта 12GB. VGG-16 има само 16 слоеви слоя и 3 напълно свързани слоя и е много по-малък от ресетния модел, който може да съдържа около сто слоя.

Фигура 1. Използване на GPU памет при използване на базова линия, политика за разпределение в цялата мрежа (лява ос). (Minsoo Rhu et al. 2016)

Сега, ако искате да обучите модел, по-голям от VGG-16, може да имате няколко възможности за решаване на проблема с ограничението на паметта.
- намалете размера на партидата, което може да попречи както на вашата тренировъчна скорост, така и на точност.
- разпределете вашия модел между множество графични процесори, което само по себе си е сложен процес.
- намалете размера на модела си, ако откриете, че не желаете да правите гореспоменатите опции или вече сте изпробвали тези опции, но те не са добри.

Или можете просто да изчакате следващото поколение графични процесори, които ще имат по-голям капацитет. Тенденцията в индустрията е към по-дълбоки и по-големи мрежи, и не искаме физическото ограничение на DRAM да бъде на път.

Наблюдение: Кой заема паметта?

Можем да разделим данните в GPU паметта в 4 категории според техните функционалности:
- Модел Параметри (Тегла)
- Функционални карти
- Градиентни карти
- Работно пространство

Първите три функционалности са лесни за разбиране. Всеки знае какви са тежестите. Картите на характеристиките са тези междинни резултати, генерирани в процеса на пренасочване. Градиентните карти са тези междинни резултати, генерирани в обратен процес. Работното пространство е буфер, използван за временни променливи / матрици на функции cuDNN. За някои функции cuDNN, потребителите трябва да предадат този буфер на ядрото като функционален параметър. Този буфер се освобождава след връщане на функцията.

Наблюдение: Функционалните карти са най-отнемащата памет
Това твърдение в това балтийско заглавие може да бъде илюстрирано със следната диаграма (Фигура 4. Разбивка на използването на GPU паметта въз основа на нейната функционалност (Minsoo Rhu et al. 2016)).

Виждаме, че като цяло колкото повече слоеве имаме, толкова повече фракция памет се отделя за карти с характеристики (триъгълниците). Можем също да видим, че този процент е почти винаги над 50% за по-големи модели като VGG-16.

Идея: Използвайте CPU памет като временен контейнер

Има един факт за картите на характеристиките: те се генерират в процеса на пренасочване, използват се веднага за следващия слой и се използват отново само веднъж по-късно, в обратния процес. Всяка функция на ядрото използва само карти с функции, които са свързани с текущия слой (обикновено само 1 тензор). Това ще остави по-голямата част от паметта мълчалива (тъй като те запазват някои данни, но не се използват) през по-голямата част от времето.

Ако обаче повечето от данните трябва да мълчат в паметта на графичния процесор, защо да не ги запазите в по-евтината CPU памет? Ето пример в AlexNet, който илюстрира какво се случва.

Пропуските, показани в лявата част, илюстрират как картите с функции остават безмълвни в паметта. Дясната част на фигурата показва идеята, която използва паметта на процесора като временен контейнер за тези карти с функции.

Компромиси: Време срещу пространство

Според документа, vDNN (съкратено за виртуализиран DNN) успешно намалява средното използване на GPU памет на AlexNet с 91% и GoogLeNet с 95%. Вероятно обаче вече сте виждали цената на това, че можете да тренирате по-бавно. Например, vDNN дава възможност за обучение на VGG-16 с размер на партидата 256 на 12GB GPU, но с 18% загуба на производителност, в сравнение с хипотетичен GPU с достатъчно памет.

Друго място, на което се появява този компромис, е размерът на работното пространство при използване на cuDNN ядра. Като цяло, колкото повече работно пространство имате, толкова по-бърз алгоритъм можете да използвате. Моля, вижте справка за библиотеката cuDNN, ако се интересувате.
Ще видим това компромис във времето и пространството чрез по-късната дискусия.

Стратегия за оптимизация: в напредък и назад процес

Вероятно вече сте знаели как vDNN оптимизира разпределението на паметта по време на напред процес. Основната стратегия е да се изтеглят карти с функции, след като са генерирани, предварително изтеглени обратно в GPU паметта, когато те са на път да бъдат използвани повторно в обратен процес. Паметта може да бъде освободена за друга употреба. Опасност от това е, че ако мрежовата топология е нелинейна, един тензор от характеристики карти може да се използва за няколко слоя, поради което те не могат да се разтоварят веднага.

При обратния процес vDNN използва по-агресивна стратегия. Тъй като за градиентните карти няма проблем с „повторна употреба по-късно“ в сравнение с картите с характеристики. Следователно те могат да бъдат пуснати след генерирането на съответните актуализации за теглото (които са доста малки в сравнение с тези карти).

Стратегия за оптимизация: CUDA поток за мениджър на памет

Ключовият компонент на vDNN е cuda поток, който управлява разпределение / освобождаване на паметта, разтоварване и предварително зареждане. Ето някои подробности:

Конвенционалното разпределение / освобождаване на cuda памет (cudaMalloc & cudaFree) са синхронни API. Тъй като те ще се случват непрекъснато в процеса на обучение, синхронните API-та не са достатъчно ефективни.

Подобно на операциите по разпределение / освобождаване, API-тата за разтоварване също трябва да бъде асинхронна. Когато е избран тензор от карти на функции за разтоварване, потокът от мениджър на паметта на vDNN ще разпредели фиксирана област от памет на хост и ще издаде не блокиращ трансфер чрез PCIe. Тези тензори на карта с функции се четат само по време на процеса на пренасочване, следователно тази процедура на прехвърляне може безопасно да се припокрива с изчислението. Само когато се извърши процедурата за разтоварване на текущия слой, програмата може да продължи следващия слой.

Операцията за предварително изтегляне е да върне натоварените карти с функции от процесора обратно към GPU по време на обратната процедура. Подобно на операциите по-горе, предварителното изтегляне също трябва да бъде асинхронно. Лесно е да се види, че съществува зависимост от данни между предварителното извличане и изчисляването на един и същ слой, следователно vDNN ще започне асинхронно изчислението на текущия слой и предварително зареждане на предишния му слой едновременно.

Цена: Как да платим цената на изпълнение за спестяванията на паметта?

Най-значителната потенциална загуба на производителността идва от неявните зависимости, въведени от разтоварването / предварително изтегляне. Помислете за случая, когато прехвърлянето на данни отнема повече време, отколкото изчисляването напред. Тази фигура ясно показва ситуацията (Фигура 9. Ефект от производителността на разтоварване и предварително изтегляне. (Minsoo Rhu et al. 2016)):

Подобна ситуация може да се случи и в обратния процес.

Нова формализация на нашия проблем: Как да постигнем най-доброто представяне при ограничен бюджет на паметта?

Както споменахме по-горе, има компромис между време и пространство, а в предишния раздел видяхме как работи компромисът. Представете си, че тренирате VGG-16 с размер на партидата 128 (което отнема 14 GB памет, ако няма разтоварване / предварително изтегляне) на 12GB GPU. Може да е твърде изгодно да използвате само около 2 GB памет, защото можете да използвате повече пространство, за да облекчите загубата на производителност. Следователно можем да реформираме проблема по този начин: Как да постигнем най-добрата ефективност при ограничен бюджет на паметта?

Конфигурирайте компромис за време и пространство: Решете дали даден слой трябва да бъде разтоварен / предварително зададен или не, и какъв алгоритъм на светене трябва да изберем.

За да получим най-добрата конфигурация, трябва да решим две неща за всеки слой: да го разтоварим / предварително да го изтеглим и какъв алгоритъм да използваме за неговия напред / назад процес (по-бързият алгоритъм означава повече пространство).

По принцип слоевете по-близо до входовете имат по-голямо разстояние за повторна употреба. Затова е за предпочитане първо да се разтоварят / предварително изтеглят тези слоеве. Не е необходимо да решаваме за всеки слой, тъй като има експоненциално много възможности по отношение на броя на слоевете. Просто трябва да изберете един слой и да кажем, че всеки слой по-близо до входовете се разтоварва / предварително зарежда, останалите слоеве имат своите тензори, останали на графичния процесор.

Да се ​​реши алгоритъмът за всеки слой също не е много практично, тъй като отново има много експоненциално много възможности по отношение на броя на слоевете. Можем просто да опростим това, като форсираме всеки слой, използвайки един и същ алгоритъм (gemm или fft или нещо подобно).

Сега имаме малко конфигурационно пространство, така че да можем да използваме алчно търсене, за да определим най-доброто решение за конфигурация. Ето фигура, която илюстрира нашето конфигурационно пространство:

Горната лява точка представлява оптималната за паметта конфигурация (свалете / предварително изтеглете всеки слой и използвайте най-бавния алгоритъм), докато долната дясна точка представлява оптималната за производителността конфигурация. Разбира се, за реалното пространство за конфигурация трябва да има решетки, а границата между изпълнимите и невъзможните елементи трябва да изглежда като стълба, но тази цифра е достатъчна, за да даде смисъл.

Следващата част от историята е да се намери възможна конфигурация, която да има най-доброто представяне. Това, което можем да направим, е да търсим конфигурацията по границата. Ако се интересувате от подробности за прилагането на тази процедура за търсене, моля, вижте оригиналната хартия (процедурата за търсене е описана в раздел 3. В).

Линк на хартия: https://arxiv.org/pdf/1602.08124.pdf

Автор: Hongyu Zhu | Редактор: Иън | Локализиран от Синхронизиран глобален екип: Xiang Chen