Параллелизм конвейера
Параллелизм конвейера был первоначально представлен в статье Gpipe и является эффективной техникой для обучения больших моделей на нескольких графических процессорах.
Предупреждение
Параллелизм конвейера является экспериментальным и может быть изменён.
Параллелизм модели с использованием нескольких графических процессоров
Обычно для больших моделей, которые не помещаются на один графический процессор, используется параллелизм модели, при котором определённые части модели размещаются на разных графических процессорах. Однако, если это делается неэффективно для последовательных моделей, процесс обучения страдает от недоиспользования графических процессоров, так как только один графический процессор активен в один момент, как показано на рисунке ниже:
На рисунке представлена модель с 4 слоями, размещёнными на 4 разных графических процессорах (вертикальная ось). Горизонтальная ось представляет обучение этой модели во времени, демонстрируя, что используется только один графический процессор в один момент (источник изображения).
Выполнение в конвейере
Для решения этой проблемы параллелизм конвейера разбивает входной мини-пакет на несколько микро-пакетов и выполняет выполнение этих микро-пакетов по конвейеру на нескольких графических процессорах. Это показано на рисунке ниже:
На рисунке представлена модель с 4 слоями, размещёнными на 4 разных графических процессорах (вертикальная ось). Горизонтальная ось представляет обучение этой модели во времени, демонстрируя, что графические процессоры используются гораздо эффективнее. Однако по-прежнему существует «пузырь» (как показано на рисунке), где определённые графические процессоры не используются. (источник изображения).
API конвейеров в PyTorch
-
class torch.distributed.pipeline.sync.Pipe(module, chunks=1, checkpoint='except_last', deferred_batch_norm=False)[source] -
Оборачивает произвольный модуль
nn.Sequentialдля обучения с помощью синхронного параллелизма конвейера. Если модуль требует много памяти и не помещается на один графический процессор, параллелизм конвейера является полезной техникой для использования при обучении.Реализация основана на статье torchgpipe.
Pipe объединяет параллелизм конвейера с контрольными точками, чтобы уменьшить пиковую память, необходимую для обучения, при одновременном минимизации недоиспользования устройств.
Вы должны разместить все модули на соответствующих устройствах и обернуть их в модуль
nn.Sequential, определяя желаемый порядок выполнения. Если модуль не содержит параметров/буферов, предполагается, что этот модуль должен выполняться на процессоре CPU, и соответствующие входные тензоры в модуль перемещаются на процессор CPU перед выполнением. Это поведение можно переопределить с помощью оболочкиWithDevice, которая может использоваться для явного указания устройства, на котором должен выполняться модуль.- Параметры
-
-
module (
nn.Sequential) – последовательный модуль, который должен быть параллелизован с помощью конвейера. Каждый модуль в последовательности должен иметь все свои параметры на одном устройстве. Каждый модуль в последовательности должен быть либо модулем nn.Module, либоnn.Sequential(для объединения нескольких последовательных модулей на одном устройстве) -
chunks (int) – количество микро-пакетов (по умолчанию:
1) -
checkpoint (str) – когда включить контрольные точки, одно из
'always','except_last', или'never'(по умолчанию:'except_last').'never'полностью отключает контрольные точки,'except_last'включает контрольные точки для всех микро-пакетов, кроме последнего, и'always'включает контрольные точки для всех микро-пакетов. -
deferred_batch_norm (bool) – использовать отложенное перемещение статистических данных
BatchNorm(по умолчанию:False). Если установлено вTrue, мы отслеживаем статистику по нескольким микро-пакетам для обновления текущей статистики по мини-пакету.
-
module (
- Возможные исключения
-
-
TypeError – модуль не является
nn.Sequential. - ValueError – недопустимые аргументы
-
TypeError – модуль не является
- Пример::
-
Конвейер из двух слоёв FC на графических процессорах 0 и 1.
>>> # Need to initialize RPC framework first. >>> 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)
Примечание
Вы можете обернуть модель
Pipeсtorch.nn.parallel.DistributedDataParallelтолько в том случае, если параметр checkpoint моделиPipeравен'never'.Примечание
Pipeв настоящее время поддерживает только внутриузловую передачу данных, но в будущем будет расширена для поддержки передачи данных между узлами. Функция forward возвращаетRRef, чтобы разрешить передачу данных между узлами в будущем, где вывод может быть на удалённом узле. Для внутриузловой передачи данных вы можете использоватьlocal_value()для получения вывода локально.Предупреждение
Pipeявляется экспериментальным и может быть изменён.-
forward(*inputs)[source] -
Обрабатывает один входной мини-пакет через конвейер и возвращает указатель
RRefна вывод.Pipeявляется довольно прозрачным модульным оболочкой. Он не изменяет входные и выходные сигнатуры базового модуля. Но есть ограничение по типам. Вход и вывод должны содержать по крайней мере один тензор. Это ограничение применяется и на границах разбиения.Последовательность входов подаётся в первую стадию конвейера как
*inputs. В результате позиционные аргументы этой функции должны соответствовать позиционным аргументам первой стадии конвейера. То же самое условие применяется к выводу одной стадии конвейера, который является входом для следующей стадии.Входной тензор разбивается на несколько микро-пакетов на основе параметра
chunks, используемого для инициализацииPipe. Размеры пакета предполагаются как первое измерение тензора, и если размер пакета меньшеchunks, количество микро-пакетов равно размеру пакета.Только тензоры разбиваются на несколько микро-пакетов, не тензорные входные данные просто дублируются в каждом микро-пакете. Для не-тензорных выходов на последней стадии конвейера они агрегируются как
Listи возвращаются пользователю. Например, если у вас есть 2 микро-пакета, возвращающие целое число 5, пользователь получит консолидированный вывод[5, 5].Все входные тензоры должны находиться на том же устройстве, что и первое разбиение конвейера.
Если тензор обернут оболочкой
NoChunk, тензор не делится на микро-пакеты и дублируется как есть, подобно не-тензорам.- Параметры
-
inputs – входной мини-пакет
- Возвращает
-
RRefдо выхода мини-пакета - Возможные исключения
-
TypeError – вход не содержит по крайней мере один тензор
- Тип возвращаемого значения
-
RRef
Соединения пропуска
Некоторые модели, такие как ResNeXt, не являются полностью последовательными и имеют соединения пропуска между слоями. При неэффективной реализации как части параллелизма конвейера предполагается, что нам нужно копировать выводы определённых слоёв через несколько графических процессоров, пока мы не достигнем графического процессора, на котором находится слой для соединения пропуска. Чтобы избежать этой издержки копирования, мы предоставляем API для сохранения и извлечения тензоров в различных слоях модели.
-
torch.distributed.pipeline.sync.skip.skippable.skippable(stash=(), pop=())[source] -
Декоратор для определения
nn.Moduleс пропусками (skip connections). Модули с декоратором называются «skippable». Эта функциональность работает корректно, даже если модуль не обернут вPipe.Каждый тензор пропуска управляется своим именем. Перед манипуляциями с тензорами пропуска, skippable модуль должен статически объявить имена тензоров пропуска с помощью параметров
stashи/илиpop. Тензоры пропуска с предварительно объявленным именем можно закрепить с помощьюyield stash(name, tensor)или извлечь с помощьюtensor = yield pop(name).Вот пример с тремя слоями. Тензор пропуска с именем «1to3» закрепляется и извлекается соответственно в первом и последнем слое:
@skippable(stash=['1to3']) class Layer1(nn.Module): def forward(self, input): yield stash('1to3', input) return f1(input) class Layer2(nn.Module): def forward(self, input): return f2(input) @skippable(pop=['1to3']) class Layer3(nn.Module): def forward(self, input): skip_1to3 = yield pop('1to3') return f3(input) + skip_1to3 model = nn.Sequential(Layer1(), Layer2(), Layer3())Один skippable модуль может закреплять или извлекать несколько тензоров пропуска:
@skippable(stash=['alice', 'bob'], pop=['carol']) class StashStashPop(nn.Module): def forward(self, input): yield stash('alice', f_alice(input)) yield stash('bob', f_bob(input)) carol = yield pop('carol') return input + carolКаждый тензор пропуска должен быть связан ровно с одной парой
stashиpop.Pipeавтоматически проверяет это ограничение при обёртке модуля. Вы также можете проверить это ограничение с помощьюverify_skippables()безPipe.
-
class torch.distributed.pipeline.sync.skip.skippable.stash(name, tensor)[source] -
Команда для закрепления тензора пропуска.
def forward(self, input): yield stash('name', input) return f(input)- Parameters
-
- name (str) – имя тензора пропуска
- input (torch.Tensor or None) – тензор для передачи в соединение пропуска
-
class torch.distributed.pipeline.sync.skip.skippable.pop(name)[source] -
Команда для извлечения тензора пропуска.
def forward(self, input): skip = yield pop('name') return f(input) + skip- Parameters
-
name (str) – имя тензора пропуска
- Returns
-
тензор пропуска, ранее закреплённый другим слоем под тем же именем
- Return type
-
None
-
torch.distributed.pipeline.sync.skip.skippable.verify_skippables(module)[source] -
Проверяет целостность skippable модулей.
Каждый тензор пропуска должен иметь только одну пару
stashиpop. Если есть одна или несколько несовпавших пар, будет поднятаTypeErrorс подробными сообщениями.Вот несколько случаев сбоя.
verify_skippables()сообщит о сбое в этих случаях:# Layer1 stashes "1to3". # Layer3 pops "1to3". nn.Sequential(Layer1(), Layer2()) # └──── ? nn.Sequential(Layer2(), Layer3()) # ? ────┘ nn.Sequential(Layer1(), Layer2(), Layer3(), Layer3()) # └───────────────────┘ ^^^^^^ nn.Sequential(Layer1(), Layer1(), Layer2(), Layer3()) # ^^^^^^ └───────────────────┘
Чтобы использовать одно имя для нескольких тензоров пропуска, они должны быть изолированы в разных именованных пространствах. См.
isolate().- Raises
-
TypeError – одна или более пар
stashиpopне совпадают.
Учебники
Следующие учебники дают хорошее представление о том, как использовать API Pipe для обучения ваших моделей с помощью остальных компонентов, предоставляемых PyTorch:
- Обучение моделей Transformer с использованием параллелизма по конвейеру
- Обучение моделей Transformer с использованием Distributed Data Parallel и параллелизма по конвейеру
Благодарности
Реализация параллелизма по конвейеру основана на реализации конвейера fairscale и torchgpipe. Мы хотим поблагодарить обе команды за их вклад и руководство по внедрению параллелизма по конвейеру в PyTorch.
© 2024, PyTorch Contributors
PyTorch has a BSD-style license, as found in the LICENSE file.
https://pytorch.org/docs/2.1/pipeline.html