През последните 10 месеца, докато работихме върху PyTorch Lightning, екипът и аз бяхме изложени на много стилове на структуриране на кода на PyTorch и идентифицирахме няколко ключови места, където виждаме хора неволно да въвеждат тесни места.

Положихме големи грижи, за да сме сигурни, че PyTorch Lightning не прави нито една от тези грешки за кода, който автоматизираме за вас, и дори се опитваме да го коригираме за потребителите, когато ги открием. Въпреки това, тъй като Lightning е просто структуриран PyTorch и вие все още контролирате целия научен PyTorch, в много случаи не можем да направим много за потребителя.

Освен това, ако не използвате Lightning, може по невнимание да въведете тези проблеми в кода си.

За да ви помогнем да тренирате по-бързо, ето 8 съвета, които трябва да знаете, че може да забавят вашия код.

Използвайте работници в DataLoaders

Тази първа грешка е лесна за коригиране. PyTorch позволява зареждане на данни за множество процеси едновременно („документация“).

В този случай PyTorch може да заобиколи заключването на GIL чрез обработка на 8 партиди, всяка в отделен процес. Колко работници трябва да използвате? Добро практическо правило е:

num_worker = 4 * num_GPU

„Този ​​отговор има добра дискусия за това.

Предупреждение: Недостатъкът е, че използването на паметта ви също ще се увеличи (източник).

Пин памет

Знаете как понякога вашата GPU памет показва, че е пълна, но сте почти сигурни, че вашият модел не използва толкова много? Това допълнително натоварване се нарича закрепена памет. т.е.: тази памет е запазена като тип „работно разпределение“.

Когато активирате pinned_memory в DataLoader, той „автоматично поставя извлечените тензори на данни в фиксирана памет и позволява по-бърз трансфер на данни към GPU с активиран CUDA“ („източник“).

Това също означава, че не трябва да се обаждате ненужно:

torch.cuda.empty_cache()

Избягвайте прехвърляне от CPU към GPU или обратно

# bad
.cpu()
.item()
.numpy()

Виждам силно използване на извикванията .item() или .cpu() или .numpy(). Това наистина е лошо за производителността, защото всяко едно от тези извиквания прехвърля данни от GPU към CPU и драматично забавя производителността ви.

Ако се опитвате да изчистите приложената изчислителна графика, използвайте вместо това .detach().

# good
.detach()

Това няма да прехвърли памет към GPU и ще премахне всички изчислителни графики, прикачени към тази променлива.

Конструирайте тензори директно върху GPU

Повечето хора създават тензори на GPU като този

t = tensor.rand(2,2).cuda()

Това обаче първо създава CPU тензор и СЛЕД това го прехвърля към GPU... това е наистина бавно. Вместо това създайте тензора директно на устройството, което искате.

t = tensor.rand(2,2, device=torch.device('cuda:0'))

Ако използвате Lightning, ние автоматично поставяме вашия модел и партида на правилния GPU за вас. Но ако създадете нов тензор във вашия код някъде (т.е.: примерен произволен шум за VAE или нещо подобно), тогава трябва да поставите тензора сами.

t = tensor.rand(2,2, device=self.device)

Всеки LightningModule има удобно извикване на self.device, което работи независимо дали сте на CPU, няколко графични процесора или TPU (т.е.: светкавицата ще избере правилното устройство за този тензор.

Използвайте DistributedDataParallel, а не DataParallel

PyTorch има два основни модела за обучение на множество GPU. Първият, DataParallel (DP), разделя партида между множество GPU. Но това също означава, че моделът трябва да бъде копиран на всеки GPU и след като градиентите се изчислят на GPU 0, те трябва да бъдат синхронизирани с другите GPU.

Това са много GPU трансфери, които са скъпи! Вместо това DistributedDataParallel (DDP)създава отделно копие на модела на всеки графичен процесор (в негов собствен процес) и прави само част от данните достъпни за този графичен процесор. Тогава е като да имаме обучение на N независими модела, с изключение на това, че след като всеки изчисли градиентите, всички те синхронизират градиентите между моделите... това означава, че прехвърляме данни между GPU само веднъж по време на всяка партида.

В Lightning можете тривиално да превключвате между двете

Trainer(distributed_backend='ddp', gpus=8)
Trainer(distributed_backend='dp', gpus=8)

Имайте предвид, че както PyTorch, така и Lightning обезсърчават използването на DP.

Използвайте 16-битова точност

Това е друг начин за ускоряване на обучението, който не виждаме много хора да използват. При 16-битово обучение части от вашия модел и вашите данни преминават от 32-битови числа към 16-битови числа. Това има няколко предимства:

  1. Използвате половината памет (което означава, че можете да удвоите размера на партидата и да намалите наполовина времето за обучение).
  2. Някои графични процесори (V100, 2080Ti) ви дават автоматични ускорявания (3x-8x по-бързи), защото са оптимизирани за 16-битови изчисления.

В Lightning това е тривиално за активиране:

Trainer(precision=16)

Забележка: Преди PyTorch 1.6 вие СЪЩО трябваше да инсталирате Nvidia Apex… сега 16-битовият е естествен за PyTorch. Но ако използвате Lightning, той поддържа и двете и автоматично превключва в зависимост от откритата версия на PyTorch.

Профилирайте своя код

Този последен съвет може да е трудно да се направи без Lightning, но можете да използвате неща като cprofiler, за да направите. В Lightning обаче можете да получите обобщение на всички обаждания, направени по време на тренировка, по два начина:

Първо, вграденият базов профайлър

Trainer(profile=True)

Което дава резултат като този:

или разширения профайлър:

profiler = AdvancedProfiler()
trainer = Trainer(profiler=profiler)

което става много гранулирано

Пълната документация за Lightning Profiler може да бъде намерена тук.

Приемане на Lightning във вашия код

PyTorch Lightning не е нищо повече от структуриран PyTorch.

Ако сте готови повечето от тези съвети да бъдат автоматизирани за вас (и добре тествани), тогава вижте този видеоклип за рефакторинг на вашия PyTorch код във формат Lightning!