Multi gpu training overview
Если обучение модели на одном графическом процессоре происходит слишком медленно или если веса модели не помещаются в VRAM, переход на обучение с несколькими графическими процессорами (или с несколькими устройствами с несколькими графическими процессорами в каждом) может быть целесообразным вариантом.
Ниже рассмотрим некоторые стратегии по масштабируемости обучения между несколькими GPU или нодами.
Глобально следует рассмотреть 3 сценария
-
Если вся модель, активации, данные оптимизатора влезают в память каждой GPU
-
Если вся модель, активации, данные оптимизатора влезают в память хотя бы одной GPU
-
Если хотя бы один слой (тензор), выходная активация с него вместе с данными оптимизатора не влезает в самую большую GPU
В первом сценарии переход на несколько нод позволит увеличить эффективный батч сайз, что сделает обучение более стабильным, а так же может его ускорить
Во втором и третьем случаях распараллеливание между gpu становится необходимостью, ведь модель физический не влезает в одну GPU
Сценарий 1.
-
DP
-
DDP
-
ZeRO
Сценарий 2.
-
Pipeline Parallel
-
Tensor Parallel
Сценарий 3.
-
Tensor parallel
Игрушечный пример
Каждый последующий пример будет использовать встроенные в торч инструменты для передачи данных между устройствами (gpu или разными серверами кластера). Так как, как я полагаю, большинство читателей с ними не знакома, думаю стоит оставить этот пример кода для лучшего понимания привыкания к инструментам торча
Отправка одного тензора между устройствами (нодами)
import torch import torch.distributed as dist dist.init_process_group(backend="gloo", # CPU only backend # https://pytorch.org/docs/stable/distributed.html init_method="tcp://node1_ip:8551", rank=0, # номер ноды world_size=2) # число нод t = torch.tensor([1.0]*10) dist.send(tensor=t, dst=1) # отправить на 1 ноду # ------ second node -------- dist.init_process_group(backend="gloo", # https://pytorch.org/docs/stable/distributed.html init_method="tcp://node1_ip:8551", rank=1, world_size=2) tensor = torch.tensor([0.0]*10) dist.recv(tensor=tensor, src=0) print(f'rank 1 received tensor {tensor} from rank 0')
DP & DDP
Овервью в доке торча Пример для линейной модели DDP в 3 строчки для lightning
DataParallel и DistributedDataParallel довольно похожие стратегии многозадачности, распишем что происходит каждый степ
DDP:
-
Каждый графический процессор обрабатывает свой минибатч данных.
-
Как только градиенты по всем весам посчитаны, все ноды синхронизируют их между собой, затем усредняют
-
Каждая нода изменяет свои веса модели на одинаковую величину, благодаря чему веса моделей не разбегаются между разными устройствами
DP:
-
GPU 0 считывает батч и затем отправляет минибатч на каждый GPU.
-
Обновленная модель реплицируется с GPU 0 на каждый GPU.
-
Каждая нода независимо выполняют forward и отправляют y на GPU0 для вычисления лоса
-
Лосс распределяются от GPU 0 ко всем графическим процессорам, каждый процессор выполняет backward
-
Градиенты с каждого графического процессора передаются в GPU 0 и усредняются.
Плюсы и минусы
+
-
DDP требует в 5 раз меньше обменов данными
-
он нагружает все гпу равномерно, когда как DP перегружает GPU:0
—
-
Проще архитектурно, есть одна мастер нода, которая общается со всеми
-
Кажется, что DDP больше страдает от не детерминированных вычислений (?)
-
DP по честному считает градиенты по всему батчу на одной гпу, а DDP усредняет градиенты для каждого мини батча. В некоторых кейсах, это приводит к меньшей стабильности обучения (когда лосс по батчу != сумме лосов по элементам)
-
При реализации в коде, вам нужно написать на один символ меньше при импорте DP, чем DDP из torch.dist
Примеры на torch
1) Обернуть модель в `model = DistributedDataParallel(model, device_ids=[local_rank])`
2) Наладить communication group между устройствами (создать обьект dist)
3)
import torch import torch.distributed as dist # ... for inp, label in dataloader: optimizer.zero_grad() out = model(inp) loss = loss_fun(out, lable) loss.backward() # --start-- каждый трэин степ синхронизировать параметры for param in model.parameters(): dist.all_reduce(param.grad.data, op=dist.reduce_op.SUM) param.grad.data /= dist.get_world_size() # --end-- optimizer.step()
Pipeline paralelism (model paralelism)
model paralelism:
Эта стратегия масштабируемости обучения предлагает разбить модель по слоям между GPU, а потом последовательно выполнять forward и backward pass-ы, передавая активации и частичные производные между гпу, затем, в конце каждого степа синхронизируя градиенты (веса модели между нодами)
pipeline paralelism:
Основная проблема модельного параллелизма в том, что каждая гпу простаивает, большую часть времени (например для 4х гпу, как на картинке — процент простоя ~80%).
Эту проблему решает pipeline параллелизм — мы разбиваем батч на минибатчи и рассчитываем их последовательно
Нативно торч поддерживает эти методы только если модель это nn.Sequential, для моделй с кастомными форвардами придется параллелить ее между gpu вручную
Также такой метод не очень подходит для сложных архитектур сетей, который нельзя разбить по слоям из-за skip connection и всякой другой экзотики в строении.
Пример на torch
# init rpc os.environ['MASTER_ADDR'] = 'localhost' os.environ['MASTER_PORT'] = '29500' torch.distributed.rpc.init_rpc('worker', rank=0, world_size=1) # Build pipe. fc1 = nn.Linear(16, 8).cuda(0) fc2 = nn.Linear(8, 4).cuda(1) model = nn.Sequential(fc1, fc2) model = Pipe(model, chunks=8) input = torch.rand(16, 16).cuda(0) output_rref = model(input)
Tensor parallelism
При обучении больших нейросетей некоторые тензоры не могут влезть на одну видеокарту целиком. Например Embedding слой трансформера с размером словаря в 128_000 токенов, а hidden_dim = 8192 будет содержать больше миллиарда параметров, что вместе с параметрами адама в fp16 будет весить десятки гигабайт.
Решение — разбить тензор на несколько гпу.
Разбиение основано на том, что поэлементной функции
(это же обобщается на любую комбинацию линейных слоев и активаций между ними.
Однако:
-
Это выходит медленно
-
Это очень сильно нагружает канал связи между гпу
ZeRO
Способ улучшить DDP (baseline) от майков
Каждая гпу, вместо хранения всей реплики модели хранит только ее часть, а по необходимости запрашивает недостающие параметры с других карточек.
Снижает занимаемый объем памяти, но более требовательно к скорости соединения
Комбинации подходов
Никто не мешает использовать сразу несколько подходов при обучении одной модели. Например дипмайнд при обучении моделей на нескольких датацентрах, вероятнее всего,
в рамках каждого датацентра они использовали PP, а затем синхронизировал градиенты между по сети между центрами (DDP).
Что еще почитать
1) Если вы занимаетесь ЛЛМ, то самое популярное решение на данный момент — DeepSpeed , паралелит обучение сильно лучше торча
2) Классная статья на медиуме
3) Овервью от huggingface в контексте ЛЛМ
ссылка на оригинал статьи https://habr.com/ru/articles/824322/
Добавить комментарий