0x00 сводка
В предыдущих статьях мы представили базовые знания о параллелизме конвейера PyTorch, механизме автоматической балансировки и сегментации данных, а в этой статье мы объединяем содержание статьи, чтобы увидеть, как обеспечить порядок выполнения прямых вычислений.
Ссылки на другие статьи о конвейерном параллелизме:
[Анализ исходного кода] Конвейер глубокого обучения, параллельный GPipe(3) -- перерасчет
[Анализ исходного кода] PipeDream (1) --- Этап профиля параллельного конвейера глубокого обучения
[Анализ исходного кода] Параллельный конвейер глубокого обучения PipeDream (6) --- Стратегия 1F1B
[Источник решен] Параллельная реализация конвейера PyTorch (1) — Основы
[Анализ исходного кода] Параллельная реализация конвейера PyTorch (2) — как разделить модель
Изображения в этой статье взяты из бумаги и исходного кода github.
0x01 Тезис
Как мы упоминали ранее, поскольку GPipe — это библиотека, основанная на TensorFlow (конечно, это продукт Google), некоторые инженеры kakaobrain использовали PyTorch для реализации GPipe и его открытого исходного кода.Это torchgpipe, и его адрес:GitHub.com/kakobrain/…, пользователи могут установить и использовать через pip install torchgpipe.
Группа авторов также опубликовала статью следующего содержания:АР Вест V.org/PDF/2004.09….
Далее мы проанализируем эту статью, которая не будет переводить всю статью, а выберет ту часть, которая тесно связана с реализацией для анализа перевода.
1.1 Введение
Одним из препятствий для параллельного обучения является то, что распространенные методы оптимизации для обучения нейронных сетей по своей сути являются последовательными. Эти алгоритмы итеративно выполняют следующие действия: для данного мини-пакета данных вычисляют его градиенты относительно функции потерь и используют эти градиенты для обновления параметров модели.
1.1.1 Параллелизм данных
При наличии больших вычислительных ресурсов параллелизм данных ускоряет общий процесс оптимизации за счет разделения мини-пакета на микропакеты и делегирования вычисления каждого микропакета доступным устройствам. При тщательной настройке гиперпараметров параллелизм данных может эффективно сократить время обучения до времени, необходимого для мини-пакета определенного размера, который может зависеть от модели, алгоритма оптимизации и данных.
Проблема с параллельным обучением данных заключается в том, что каждое устройство имеет собственную версию модельной сети для выполнения подзадач, а параметры модельной сети необходимо синхронизировать после каждого обновления параметров. Это может привести к большой коммуникационной нагрузке, когда нужно синхронизировать много параметров.
Однако параллелизм данных неприменим, когда модель настолько велика, что градиент невозможно вычислить, даже если модель не подходит для одной машины.
1.1.2 Параллелизм модели
Параллелизм моделей — это метод обучения огромной модели путем разделения модели на части и размещения их на разных устройствах. Каждое устройство вычисляет только небольшую часть модели и обновляет параметры только в этой части. Однако модельный параллелизм страдает из-за своего «недостаточно используемого» поведения. Поскольку большинство нейронных сетей состоят из ряда слоев, устройство, хранящее последнюю часть модели, должно ждать, пока не завершится вычисление устройства, содержащего раннюю часть модели.
Возможным решением является использование градиентной контрольной точки, которая сохраняет только подмножество активаций и при необходимости пересчитывает отброшенные активации, экономя память. Очевидно, что это требует двух вычислений для некоторых частей модели и увеличивает общее время обучения.
В последующих разделах мы обсудим, как разбить прямые и обратные процессы на подзадачи (при определенных предположениях), опишем стратегию распределения устройств для микропакетного конвейерного параллелизма и продемонстрируем требуемый порядок выполнения для каждого устройства. Также обсуждаются тонкости реализации оптимальных временных рамок для конвейерного параллелизма в PyTorch и объясняется, как torchgpipe решает эти проблемы.
Кроме того, мы ослабляем предположение о том, что модели составляются последовательно, и предоставляем способ представления моделей с использованием соединений с длинным пропуском, чтобы по-прежнему применялся конвейерный параллелизм без потери эффективности.
1.2 Определение модели
Предположим, у нас есть нейронная сеть, состоящая из ряда подсетей. Предположим, что эти подсети равны , а их параметры равны соответственно, тогда вся сеть равна:
Поскольку Nuggets в последнее время не могут разобрать формулу, они могут использовать только изображения для отображения текста, пожалуйста, поймите.
или пожалуйста переместитеwoo woo woo.cn blog on.com/Ross i XYZ/afraid/…
Первые три F — это прямое распространение трех подсетей, а последние три B — обратное распространение трех подсетей.
Ниже представлен первый микропакет, который последовательно завершает прямое и обратное распространение трех подсетей.
Учитывая набор задач и и пул устройств, которые могут работать параллельно, разные стратегии распараллеливания имеют свои правила назначения задач устройствам.
После разрешения зависимостей каждое устройство выполняет одну или несколько назначенных задач. В приведенной выше настройке все зависимости задач имеют один и тот же индекс микропакета i. Таким образом, назначая задачи с разными индексами микропакетов разным устройствам, задачи можно эффективно распараллелить, что является параллелизмом данных.
1.3 График расчета GPipe
Стратегия конвейерного параллелизма заключается в распределении задач в соответствии с индексом раздела j так, чтобы раздел j целиком находился на устройстве j. В дополнение к этому также обязательно, что должно быть выполнено до и должно быть завершено до выполнения.
В дополнение к микропакетной конвейерной обработке GPipe еще больше снижает требования к памяти, используя контрольные точки градиента для каждого файла . Поскольку th устройство выполняется только каждый раз, при вычислении необходимо получить только карту активации .
Поскольку прямой проход вычисляется непосредственно перед выполнением, мы уменьшаем потребление памяти в m раз. Кроме того, когда устройство ожидает, может произойти пересчет, и эта информация показана на следующем рисунке:
Пунктирные стрелки указывают порядок выполнения между независимыми задачами из-за введения порядка микропакетов. Цвета обозначают разные устройства.
Заметим, что пересчет последней микропартии, т. е. , здесь излишен.
Это связано с тем, что на j-м устройстве последней задачей в прямом проходе является , , поэтому отбрасывание промежуточных активаций в прямом проходе и повторное вычисление их в начале обратного прохода не уменьшает память, а только замедляет конвейер. Поэтому на рисунке опущен.
1.4 Порядок выполнения по устройству
Таким образом, при конвейерном параллелизме (с контрольными точками) каждому устройству назначается набор задач в определенном порядке. Как только зависимости между устройствами будут удовлетворены, каждое устройство будет выполнять заданную задачу одно за другим. Однако на этой картинке не хватает одного компонента — передачи данных между устройствами. В иллюстративных целях полное выполнение устройства j должно следовать показанной последовательности. Кроме того, для большей ясности операции передачи данных прямо обозначаются как «получение» и «отправка».
Для удобства библиотека предоставляет подмодуль torchgpipe.balance для расчета разделов, чтобы сделать разницу в ресурсах между попарными разделами как можно меньше. Занятость ресурсов рассчитывается путем профилирования. В частности, используется алгоритм из [2] Imre B'ar'any and Victor S Grinberg. Block Partitions of Sequences. Israel Journal of Mathematics, 206(1):155–164.
1.5. Трудности реализации PyTorch
Нашей главной задачей является эффективность. Чтобы конвейерный параллелизм работал должным образом, задачи должны распределяться на каждое устройство в правильном порядке. Реализация этого в Pytorch имеет несколько сложностей.
-
Во-первых, из-за того, что PyTorch определяет стиль запуска и его активное поведение при выполнении (в отличие от структуры «создай и запусти»), функции ядра динамически публикуются на каждом устройстве.
- Следовательно, код хоста должен быть тщательно спроектирован таким образом, чтобы не только задачи, привязанные к устройству, выполнялись в правильном порядке на каждом устройстве, но и чтобы избежать задержек на устройстве из-за того, что интерпретатор Python не может запросить его заранее (асинхронно). с ЦП) для выполнения задачи.
- Эта задержка может возникнуть, когда определенные задачи интенсивно используют ЦП или включают много дешевых вызовов ядра. В качестве решения torchgpipe вводит детерминированный тактовый цикл, который определяет общий порядок задач.
-
Во-вторых, вычислительный граф обратного распространения динамически строится во время прямого распространения. Другими словами, «он избегает конкретизации «прямого графа» и записывает только то, что необходимо для дифференциальных вычислений». Поскольку PyTorch не записывает графы прямых вычислений и не поддерживает ленту градиентов, механизм автоградации PyTorch выполняет только обратное распространение графов вычислений. Это означает, что механизм автозагрузки не может работать точно в порядке, обратном выполнению прямого процесса, если это не предусмотрено структурой графа. Чтобы решить эту проблему, torchgpipe разработал пару основных функций, названных «fork» и «join», для динамического создания явных зависимостей в графе обратных вычислений.
- В-третьих, при неаккуратном управлении связь между несколькими устройствами может привести к двусторонней синхронизации. Это приводит к недоиспользованию, поскольку отправитель может ожидать синхронизации с получателем, даже если нет явной зависимости между репликой и следующей задачей в очереди, и наоборот. torchgpipe позволяет избежать этой проблемы, используя поток CUDA не по умолчанию, чтобы реплики не блокировали вычисления, если вычислениям не приходится ждать данных.
-
Наконец, torchgpipe пытается ослабить ограничения параллелизма микропакетного конвейера (модель должна быть последовательной).
-
Хотя в принципе любую нейронную сеть можно написать в последовательной форме, для этого требуется заранее знать весь вычислительный граф, чего нет в PyTorch. В частности, если есть тензор, который перескакивает с одного слоя в device на другой слой в device , этот тензор будет скопирован на все промежуточные устройства, потому что torchgpipe не может узнать его заранее. Чтобы избежать этой проблемы, мы разрабатываем интерфейс, чтобы указать, какие промежуточные тензоры пропускаются и какие слои их используют.
-
1.6 Резюме
Мы суммируем текущую основную трудность и представляем следующую работу.
-
исходный трубопроводСостояние следующее:
- Стратегия конвейерного параллелизма заключается в распределении задач в соответствии с индексом раздела j так, чтобы раздел j целиком находился на устройстве j.
- Устройство, содержащее позднюю часть модели, должно дождаться завершения расчета устройства, содержащего раннюю часть модели.
- целевой конвейерСостояние следующее:
-
текущая проблема:
- При разделении на несколько микропакетов обязательно, чтобы они завершились до , и должны были завершиться до выполнения .
- Вычислительный граф обратного распространения динамически строится во время прямого распространения. PyTorch не записывает графы прямых вычислений и не поддерживает ленту градиентов, а механизм автоградации PyTorch только выполняет графы вычислений в обратном направлении. Это означает, что механизм автозагрузки не может работать точно в порядке, обратном выполнению прямого процесса, если это не предусмотрено структурой графа.
)
-
текущая сложность:
- Как я могу опубликовать эти связанные с устройством задачи в правильном порядке на каждом устройстве, чтобы избежать задержки выполнения задач на устройстве (асинхронно с ЦП) из-за того, что интерпретатор Python не может запросить их заранее.
- Как установить кросс-девайсные зависимости между этими небольшими партиями.
-
План реализации:
-
Как обеспечить правильный порядок выполнения?torchgpipe представил детерминированный тактовый цикл (deterministic clock-cycle), который дает общий порядок задач.
-
Как гарантировать динамические явные зависимости в вычислительном графе?Для каждого плана запуска, сгенерированного clock_cycles:
- Используйте функцию забора для вызова «разветвления» и «объединения», чтобы динамически создавать явные зависимости обратного распространения в графе обратных вычислений.
- Вычислить с помощью calculate(schedule, skip_trackers, in_queues, out_queues) .
-
В этой статье сначала рассматривается, как обеспечить правильный порядок выполнения при расчете вперед.
0x02 Порядок выполнения
Давайте взглянем на алгоритм детерминированного тактового цикла (прямая зависимость: детерминированный тактовый цикл). Эта сортировка специально используется при прямом распространении, и прямое распространение вычисляется по одному в соответствии с этим алгоритмом.
Вообще говоря, расчет прямого распространения выполняется в соответствии со структурой модели, но поскольку параллелизм конвейера особенный, модель была разделена, поэтому torch-gpipe должен самостоятельно обеспечивать последовательность выполнения прямого распространения для выполнения каждого микропакета.
2.1 Содержание статьи
Общий порядок задач определяется кодом узла в прямом проходе. Каждое устройство неявно понимает зависимости между задачами через порядок распределения ЦП. В идеале, если задачи могут быть назначены устройствам бесплатно, ЦП может назначать задачи устройствам в любом порядке, если порядок внутри устройства правильный. Однако это предположение нереалистично, поскольку запуск функций ядра на графических процессорах не является бесплатным для ЦП, для передачи памяти между графическими процессорами может потребоваться синхронизация или задачи интенсивно используют ЦП. Поэтому для минимизации задержки от ЦП мы упорядочиваем все задачи по «расстоянию до узла».
Мы называем эту схему детерминированным алгоритмом тактового цикла. В этом алгоритме процессор выполняет тактовые циклы от счетчика до . В k-м такте для этих индексов:
- Все функции ядра копирования данных, требуемых задачей, выполняются в первую очередь.
- Вычислительные ядра, используемые для выполнения задач, затем регистрируются на соответствующем устройстве (поскольку задачи в одном и том же тактовом цикле независимы, многопоточность безопасна).
2.2 Анализ
Давайте взглянем на фотографии бумаги, а именно:
- Когда часы 1, запустите график на
- Когда часы 2, бегите по графику. Это запустить одну сетку вправо, и в это же время в тренировку входит второй микробатч, то есть бежит.
- Когда часы 3, бегите по графику. То есть прогоняем одну сетку вправо, прогоняем одну сетку вправо, и в процесс обучения вступает третий микробатч, то есть запускается.
- Когда часы 4 , бегите по графику. То есть запустить одну сетку вправо, запустить одну сетку вправо, и в это же время в процесс обучения вступает четвертый микробатч, то есть пробежки.
- И так далее.....
По рисунку мы видим, что
- Расстояние шага до равно 1, и до него можно добраться за один шаг.
- Шаговое расстояние до равно 2, и до него можно добраться за два шага.
Эту логику хорошо видно из рисунка ниже. Поэтому алгоритм этих часов заключается в использовании расстояния до задач для сортировки всех задач. Это очень похоже на бросок камня в воду, и рябь воды распространяется от ближней точки к дальней слой за слоем от точки падения.
Здесь цвета представляют разные устройства.
2.3 Код
Давайте снова посмотрим на код. Первый — генерировать тактовые циклы, здесь:
- min(1+k, n) — это максимальное количество устройств (разделов), которые могут быть запущены в такте k.
- max(1+k-m, 0) — это наименьший микропакет, который может быть запущен на такте k.
Таким образом, конечная возвращаемая последовательность — это последовательность (индекс микропакета, индекс раздела), которая может быть запущена на такте k.
def clock_cycles(m: int, n: int) -> Iterable[List[Tuple[int, int]]]:
"""Generates schedules for each clock cycle."""
# m: number of micro-batches
# n: number of partitions
# i: index of micro-batch
# j: index of partition
# k: clock number
#
# k (i,j) (i,j) (i,j)
# - ----- ----- -----
# 0 (0,0)
# 1 (1,0) (0,1)
# 2 (2,0) (1,1) (0,2)
# 3 (2,1) (1,2)
# 4 (2,2)
# 我们解析一下,这里 k 就是时钟数,从1开始,最多时钟序号就是 m+n-1。
# min(1+k, n) 就是在 k 时钟时候,可以启动的最大device数目
# max(1+k-m, 0) 就是在 k 时钟时候,可以启动的最小batch
for k in range(m+n-1):
yield [(k-j, j) for j in range(max(1+k-m, 0), min(1+k, n))]
Установив m = 4, n = 3, результат решения (4,3):
[(0, 0)]
[(1, 0), (0, 1)]
[(2, 0), (1, 1), (0, 2)]
[(3, 0), (2, 1), (1, 2)]
[(3, 1), (2, 2)]
[(3, 2)]
Поскольку в документе есть пример диаграммы, и эта диаграмма не совсем совпадает с аннотацией и кодом, для лучшего объяснения мы следуем диаграмме, поскольку изображение начинается с , поэтому мы исправим аннотацию следующим образом:
# 0 (0,0) ----> clock 1 运行图上的 (1,1)
# 1 (1,0) (0,1) ----> clock 2 运行图上的 (2,1) (1,2)
# 2 (2,0) (1,1) (0,2) ----> clock 3 运行图上的 (3,1) (2,2) (1,3)
# 3 (2,1) (1,2) ----> clock 4 运行图上的 (3,2) (2,3)
# 4 (2,2) ----> clock 5 运行图上的 (3,3)
Мы изменили код решения, чтобы напечатать правильный индекс, чтобы каждый мог лучше сопоставить код и изображение.
m=4 # m: number of micro-batches
n=3 # n: number of partitions
for k in range(m + n - 1):
print( [(k - j + 1 , j +1 ) for j in range(max(1 + k - m, 0), min(1 + k, n))] )
打印是:
[(1, 1)] # 第 1 轮训练计划 & 数据
[(2, 1), (1, 2)] # 第 2 轮训练计划 & 数据
[(3, 1), (2, 2), (1, 3)] # 第 3 轮训练计划 & 数据
[(4, 1), (3, 2), (2, 3)] # 第 4 轮训练计划 & 数据
[(4, 2), (3, 3)] # 第 5 轮训练计划 & 数据
[(4, 3)] # 第 6 训练计划 & 数据
Давайте посмотрим на схему сборочной линии.
Мы рисуем приведенный выше вывод в соответствии с диаграммой конвейера в качестве сравнения.
Видно, что за первые 4 такта в cuda:0 поступило 4 микропакета соответственно (1,1) (2,1) (3,1) (4,1) . Затем, в соответствии с порядком, заданным алгоритмом clock_cycles, на каждой итерации (тактовом цикле) выполняются разные расписания, и после 6 тактов завершается первый раунд прямой операции. Это формирует трубопровод.
Преимущество конвейерной обработки заключается в том, что при правильной настройке количества микропакетов можно получить максимальную отдачу от всех устройств, работающих в каждом такте. Напротив, собственный конвейер может сделать активным только одно устройство за раз.
+ + + + + + +
| | | | | | |
| | | | | | |
cuda:0 | (1,1) | (2,1) | (3,1) | (4,1) | | |
| | | | | | |
| | | | | | |
| | | | | | |
| | | | | | |
cuda:1 | | (1,2) | (2,2) | (3,2) | (4,2) | |
| | | | | | |
| | | | | | |
| | | | | | |
| | | | | | |
cuda:2 | | | (1,3) | (2,3) | (3,3) | (4,3) |
| | | | | | |
| | | | | | |
| | | | | | |
| clock 1 | clock 2 | clock 3 | clock 4 | clock 5 | clock 6 |
+ + + + + + +
+------------------------------------------------------------------------------> Time
Направление конкретного пакета данных:
+ + + + + + +
| | | | | | |
cuda:0 | (1,1) | (2,1) | (3,1) | (4,1) | | |
| + | + | + | + | | |
| | | | | | | | | | |
| | | | | | | +----------+ | |
| | | | | +-----------+ | | | |
| | | +------------+ | | | | | |
| | | | | | | | | | |
| +------------+ | | | | | | | |
| | | | | | | | | | |
| | | | v | v | v | |
| | v | | | | |
cuda:1 | | (1,2) | (2,2) | (3,2) | (4,2) | |
| | + | + | + | + | |
| | | | | | | | | | |
| | | | | | | | +-------------+ |
| | | | | | +----------+ | | |
| | | | +------------+ | | | | |
| | +-----------+ | | | | | | |
| | | | | v | v | v |
| | | v | | | |
cuda:2 | | | (1,3) | (2,3) | (3,3) | (4,3) |
| | | | | | |
| | | | | | |
| | | | | | |
| clock 1 | clock 2 | clock 3 | clock 4 | clock 5 | clock 6 |
+ + + + + + +
+-----------------------------------------------------------------------------------> Time
2.4 Использование
В классе Pipeline мы видим, что расчет запускается по такту, так что при прямом распространении по этой последовательности он распространяется, как водная рябь.
def run(self) -> None:
"""Runs pipeline parallelism.
It modifies the given batches in place.
"""
batches = self.batches
partitions = self.partitions
devices = self.devices
skip_layout = self.skip_layout
m = len(batches)
n = len(partitions)
skip_trackers = [SkipTrackerThroughPotals(skip_layout) for _ in batches]
with spawn_workers(devices) as (in_queues, out_queues):
for schedule in clock_cycles(m, n): # 这里使用,给出了执行序列计划,后续按照这个来执行
self.fence(schedule, skip_trackers) # 构建后向传播依赖关系
self.compute(schedule, skip_trackers, in_queues, out_queues) # 进行计算
На этом анализ процесса прямого распространения завершен, в следующей статье мы разберем зависимости.
0xEE Личная информация
★★★★★★Думая о жизни и технологиях★★★★★★
Публичный аккаунт WeChat:мысли Росси
ссылка 0xFF
Учебник по редактированию формул в уценке
docs.NVIDIA.com/Bulky/Bulky-Day…
Обучение CUDA: краткое изложение основ
Использование Stream в очерках CUDA
Accelerating Wide & Deep Recommender Inference on GPUs
HugeCTR: High-Performance Click-Through Rate Estimation Training
обсудить.py torch.org/he/how-to-cheat…
Пишите на torch.org/docs/notes/…