Перейти к содержимому
ПереводыStanford

CS336, лекции 7–8: параллелизм — от коллективных операций до 3D-параллелизма

Перевод лекций весны 2025 года: коллективные операции и сеть между GPU, параллелизм по данным и ZeRO 1–3 (FSDP), конвейерный, тензорный и последовательный параллелизм, память активаций, правила 3D-параллелизма и примеры Llama 3, DeepSeek, Gemma 2; затем — те же идеи в коде на torch.distributed.

  • Лекция 7 — карта всех видов параллелизма. Её прямой аналог в курсе — урок 5.4.3 «Распределённое обучение»; экспертный параллелизм — к модулю 5.3.
  • Лекция 8 — те же идеи в 400 строках кода на torch.distributed. После неё сделайте урок 5.7.4 «Четыре видеокарты: DDP и FSDP на практике» — там код запускается на ваших 4 × RTX 4080.
  • Главное отличие вашей машины от кластера Стэнфорда: между картами нет NVLink, только PCIe. Поэтому выводы лекции о тензорном параллелизме («только внутри узла с быстрой связью») для вас означают: тензорный параллелизм на 4 × RTX 4080 почти всегда невыгоден, а DDP и FSDP работают, если редко синхронизироваться. Подробнее — в примечаниях ниже.
  • Это теория для второй части задания 2 CS336 (DDP и шардинг оптимизатора), см. путеводитель по заданию 2.

  • Понять системные сложности обучения огромных моделей.
  • Разные парадигмы распараллеливания и почему их используют несколько сразу.
  • Как обычно выглядят большие запуски обучения.
  • Часть 1: основы сетей для LLM.
  • Часть 2: разные формы параллельного обучения LLM.
  • Часть 3: масштабирование и обучение больших LM с параллелизмом.

Пределы масштабирования на GPU — вычисления (4)

Заголовок раздела «Пределы масштабирования на GPU — вычисления (4)»

У масштабирования на одном GPU есть пределы. [Слева — слайд Билла Далли о росте производительности одного чипа в 1000 раз; справа — график роста производительности суперкомпьютеров от мегафлопсов в 1990-х до экзафлопсов в 2020-х.] У самых быстрых суперкомпьютеров мира — экзафлопсы вычислений.

Модели становятся очень большими… [График размера моделей по годам в логарифмической шкале: ELMo (94 млн) → BERT-Large (340 млн) → GPT-2 (1,5 млрд) → Megatron-LM (8,3 млрд) → T5 (11 млрд) → Turing-NLG (17,2 млрд) → GPT-3 (175 млрд) → Megatron-Turing NLG (530 млрд).] На одном GPU большинство таких больших моделей не помещается!

Что делать? Параллелизм на нескольких GPU и машинах (6)

Заголовок раздела «Что делать? Параллелизм на нескольких GPU и машинах (6)»

[Схема узла DGX: два CPU, коммутаторы PCIe, сетевые карты InfiniBand (HCA), восемь GPU, соединённых через NVSwitch. Подписи: HDR InfiniBand — 50 ГТ/с на линию, PCI Express 4.0 — 16 ГТ/с на линию, xGMI-2 — 16 ГТ/с, NVLink 3.0 — 400 ГТ/с.]

  • Параллелизм внутри узла — через высокоскоростные соединения.
  • Высокоскоростной параллелизм между узлами.

Разделяем требования к памяти и вычислениям между GPU и машинами.

Но сначала — основы коллективной коммуникации (7)

Заголовок раздела «Но сначала — основы коллективной коммуникации (7)»

[Схемы для четырёх процессов (rank 0–3):]

  • All-reduce — у каждого свой вектор, после операции у всех одинаковая сумма всех векторов.
  • Broadcast — данные одного процесса копируются всем.
  • Reduce — сумма векторов всех процессов оказывается у одного.
  • All-gather — каждый вносит свой кусок, после операции у всех — склейка всех кусков.
  • Reduce-scatter — суммируем векторы, и каждый получает свою часть суммы.

Важная деталь: all-reduce против reduce-scatter + all-gather (8)

Заголовок раздела «Важная деталь: all-reduce против reduce-scatter + all-gather (8)»

All-reduce можно реализовать в два шага: reduce-scatter и all-gather. [Схема: у 4 GPU векторы A, B, C, D, каждый из четырёх частей; после reduce-scatter у каждого GPU — сумма своей четверти; после all-gather у всех — полная сумма.]

Важно: в режиме, ограниченном пропускной способностью, лучше этого сделать нельзя.

  • Сеть TPU: тороидальная сетка (каждый чип связан с соседями, края сетки замкнуты в кольцо).
  • Сеть GPU: «все со всеми» (до 256 GPU). [Схемы DGX A100 SuperPOD и H100 SuperPOD с полностью связанными коммутаторами NVLink и таблица пропускной способности.]
  • Новая единица вычислений — дата-центр.
  • Чего мы хотим от масштабирования на много машин:
    • линейного масштабирования памяти (максимальное число параметров растёт с числом GPU);
    • линейного масштабирования вычислений (FLOPs модели растут линейно с числом GPU).
  • Простых примитивов коллективной коммуникации.

Часть 2. Стандартные примитивы распараллеливания LLM (11)

Заголовок раздела «Часть 2. Стандартные примитивы распараллеливания LLM (11)»

Как распараллелить LLM? Три важные идеи:

  • Параллелизм по данным (data parallelism):
    • наивный параллелизм по данным;
    • уровни ZeRO 1–3.
  • Параллелизм по модели (model parallelism):
    • конвейерный параллелизм (pipeline parallel);
    • тензорный параллелизм (tensor parallel).
  • Параллелизм по активациям:
    • последовательный параллелизм (sequence parallel).

Отправная точка: представим, что мы делаем обычный SGD:

θt+1=θt−η∑i=1B∇f(xi).\theta_{t+1} = \theta_t - \eta \sum_{i=1}^{B} \nabla f(x_i).

Наивный параллелизм: делим BB примеров батча между MM машинами. Обмениваемся градиентами, чтобы синхронизироваться.

Как это работает?

  • Масштабирование вычислений — каждый GPU получает B/MB/M примеров.
  • Накладные расходы на связь — каждый батч передаётся 2 × #params. Приемлемо, если батчи большие.
  • Масштабирование памяти — никакого. Каждому GPU нужно как минимум #params.

Что не так с наивным параллелизмом по данным? (13)

Заголовок раздела «Что не так с наивным параллелизмом по данным? (13)»

[Рисунок: четыре GPU, на каждом — полная копия модели, данные делятся между ними.] Похоже, проблемой будет память — мы копируем параметры модели на каждый GPU. Присмотримся…

Что не так с наивным параллелизмом? — Память (14)

Заголовок раздела «Что не так с наивным параллелизмом? — Память (14)»

Наша ситуация с памятью на самом деле ужасна. В зависимости от точности… Нужно 5 копий весов и 16 байт на параметр!

  • 2 байта — параметры модели в FP16/BF16;
  • 2 байта — градиенты в FP16/BF16;
  • 4 байта — главные веса (master weights) в FP32 (в них накапливаются обновления SGD);
  • 4 (или 2) байта — первый момент Adam в FP32/BF16;
  • 4 (или 2) байта — второй момент Adam в FP32/BF16.

Последние три пункта — это «состояние оптимизатора».

ZeRO — решение проблемы памяти в параллелизме по данным (15)

Заголовок раздела «ZeRO — решение проблемы памяти в параллелизме по данным (15)»

Главная идея: разделить дорогие части (состояние) и использовать эквивалентность с reduce-scatter.

[Рисунок из статьи ZeRO: на Nd=64N_d = 64 GPU, модель Ψ=7,5\Psi = 7{,}5 млрд параметров, K=12K = 12 байт состояния оптимизатора на параметр. Базовый вариант: (2+2+K)⋅Ψ(2 + 2 + K) \cdot \Psi = 120 ГБ на GPU. PosP_{os} (шардинг состояния оптимизатора): 2Ψ+2Ψ+KΨNd2\Psi + 2\Psi + \frac{K \Psi}{N_d} = 31,4 ГБ. Pos+gP_{os+g} (плюс градиенты): 2Ψ+(2+K)ΨNd2\Psi + \frac{(2+K)\Psi}{N_d} = 16,6 ГБ. Pos+g+pP_{os+g+p} (плюс параметры): (2+2+K)ΨNd\frac{(2+2+K)\Psi}{N_d} = 1,9 ГБ.]

ZeRO, этап 1: шардинг состояния оптимизатора (16)

Заголовок раздела «ZeRO, этап 1: шардинг состояния оптимизатора (16)»

Идея:

  • разделить состояние оптимизатора (первый и второй моменты) между GPU;
  • у всех есть параметры и градиенты.

Каждый процесс отвечает за обновление подмножества параметров (соответствующего его доле).

  • Шаг 1. Каждый считает полный градиент на своей части батча.
  • Шаг 2. Reduce-scatter градиентов — стоимость связи #params.
  • Шаг 3. Каждая машина обновляет свои параметры, используя свой градиент и состояние.
  • Шаг 4. All-gather параметров — стоимость связи #params.

Сравнение ZeRO-1 и наивного параллелизма по данным (18)

Заголовок раздела «Сравнение ZeRO-1 и наивного параллелизма по данным (18)»
Наивный DDPZeRO, этап 1
Примитив связиодин all-reduce (градиентов)один reduce-scatter (отправка градиентов) + all-gather (сбор параметров)
Стоимость связи2 × #params2 × #params
Память(4+K)⋅(4 + K) \cdot #params(4+K/Ngpu)⋅(4 + K/N_{gpu}) \cdot #params

ZeRO-1 — это бесплатный (в режиме, ограниченном пропускной способностью) выигрыш в памяти.

ZeRO, этап 2: простое расширение — шардинг градиентов (19)

Заголовок раздела «ZeRO, этап 2: простое расширение — шардинг градиентов (19)»

Воодушевлённые успехом, шардируем ещё больше. [Pos+gP_{os+g}: 2Ψ+(2+K)ΨNd2\Psi + \frac{(2+K)\Psi}{N_d} = 16,6 ГБ.]

Идея:

  • градиенты (розовые доли) тоже хранить разделёнными между машинами;
  • использовать (примерно) те же приёмы, что в этапе 1.

Сложность: полный вектор градиентов нельзя создать целиком ни разу, но каждый процесс должен посчитать полный градиент (ведь это параллелизм по данным).

  • Шаг 1. Каждый постепенно идёт назад по графу вычислений.
    • Шаг 1а. Посчитав градиенты слоя, сразу делаем reduce, чтобы отправить их нужному процессу.
    • Шаг 1б. Как только градиенты больше не нужны в обратном графе, сразу освобождаем их.
  • Шаг 2. Каждая машина обновляет свои параметры, используя свой градиент и состояние.
  • Шаг 3. All-gather параметров.

[Pos+g+pP_{os+g+p}: (2+2+K)ΨNd\frac{(2+2+K)\Psi}{N_d} = 1,9 ГБ.] До сих пор почти всё доставалось бесплатно… попробуем решить все проблемы с памятью.

Идея:

  • шардировать всё — включая параметры!
  • использовать те же идеи «постепенной коммуникации и вычислений»;
  • отправлять и запрашивать параметры по требованию, проходя по графу вычислений.

Можно ли сделать это с малыми накладными расходами?

[Схема из учебника PyTorch FSDP: для каждого блока модели — all-gather весов, локальный прямой проход, освобождение полных весов; в обратном проходе — снова all-gather, локальный обратный проход, reduce-scatter градиентов, освобождение весов; в конце — локальное обновление.]

Стоимость связи — 2 all-gather (#params), 1 reduce-scatter (#params).

Разберём пример FSDP, чтобы увидеть важные идеи. [Временная диаграмма из статьи о PyTorch FSDP: на CPU ставятся в очередь all-gather (AG) и вычисления; в отдельном потоке связи AG0, AG1, AG2 идут параллельно с прямым проходом FWD0, FWD1, FWD2; в обратном проходе reduce-scatter (RS) перекрываются с BWD.]

  • Постепенные вычисления и связь: параметры и градиенты запрашиваются/отправляются и сразу освобождаются.
  • Перекрытие связи и вычислений (пример (W1W0+W2W0)x=y(W_1 W_0 + W_2 W_0) x = y): все all-gather запускаются сразу, пока идёт прямой проход, — это маскирует стоимость связи.

Распределённый параллелизм по данным стоит 2 × #params на связь. А ZeRO?

  • ZeRO-1 — 2 × #params, это бесплатно, так что его можно всегда включать.
  • ZeRO-2 — 2 × #params, это (почти) бесплатно (если не считать накладных расходов).
  • ZeRO-3 — 3 × #params: в 1,5 раза дороже по связи, но это неплохо! (если не считать задержки…)

К тому же это концептуально очень просто — пишешь обёртку для блока FSDP.

Чистое обучение в BF16 (с суммированием Кэхэна) работает, и состояния оптимизатора не такие тяжёлые. Пусть всё в BF16, кроме главных весов, — 12 байт на параметр. На 8 × A100 80 ГБ:

Макс. размер (параметров)Формула байт на параметр
Базовый6,66… млрд12
ZeRO-116 млрд5
ZeRO-224,62 млрд2 (параметры) + 10 (градиенты + состояние) / 8
ZeRO-353,33 млрд12 / 8

Остающиеся проблемы параллелизма по данным — вычисления (26)

Заголовок раздела «Остающиеся проблемы параллелизма по данным — вычисления (26)»

При параллелизме по данным число машин меньше размера батча (а около этой границы накладные расходы на связь высоки). И у роста батча убывающая отдача. [График «Predicted Training Speed» из работы о критическом размере батча: при малом батче — «идеальное масштабирование», после шумового масштаба BnoiseB_{noise} — «неэффективное масштабирование».]

Остающиеся проблемы параллелизма по данным — модели не помещаются (27)

Заголовок раздела «Остающиеся проблемы параллелизма по данным — модели не помещаются (27)»
  • ZeRO-1 и ZeRO-2 не позволяют масштабировать память.
  • ZeRO-3 хорош в принципе, но может быть медленным и не уменьшает память активаций.

[График из статьи Megatron-LM: достигнутые TFLOP/s на GPU при росте числа GPU от 768 до 1920 — у ZeRO-3 для моделей 175 и 530 млрд падает, у конвейерно-тензорного параллелизма (PTD-P) остаётся ровным.]

Нужны лучшие способы разрезать модель…

За пределами параллелизма по данным — параллелизм по модели (28)

Заголовок раздела «За пределами параллелизма по данным — параллелизм по модели (28)»

Масштабируем память (не меняя размер батча) параллелизмом по модели.

Что такое параллелизм по модели:

  • он делит параметры между GPU (как ZeRO-3)…
  • но пересылает активации (а ZeRO-3 пересылает параметры).

Разберём два вида параллелизма:

  1. Конвейерный параллелизм.
  2. Тензорный параллелизм (+ последовательный).

Они соответствуют двум разным способам разрезать модель.

[Схема: слои 0–3 на GPU 0–3; прямой проход идёт слева направо, обратный — справа налево.] Послойный параллелизм режет модель по слоям и назначает подмножества слоёв разным GPU. Активации и частичные градиенты передаются туда и обратно.

Загрузка при послойном параллелизме ужасна… При nn GPU каждый GPU активен 1n\frac{1}{n} времени. [Диаграмма-«лесенка»: F0, F1, F2, F3 выполняются по очереди, затем B3, B2, B1, B0.] Каждый GPU большую часть времени простаивает, ожидая, пока обратный проход дойдёт до него.

[Диаграмма: батч разбит на 4 микробатча; GPU 0 считает F0,0, F0,1, F0,2, F0,3 и не ждёт, GPU 1 начинает с задержкой на один шаг и т. д.; посередине — «пузырь» простоя.]

Решение — конвейерный параллелизм. Обрабатываем «микробатчи» (здесь 4). Отправили первый микробатч — начинаем считать второй.

Отношение времени пузыря к полезным вычислениям:

nstages−1nmicro,\frac{n_{stages} - 1}{n_{micro}},

так что нужен большой размер батча!

Конвейеры кажутся ужасными. Зачем их используют?

  1. Конвейеры экономят память (по сравнению с DDP).
  2. У конвейеров могут быть хорошие свойства связи (по сравнению с FSDP): объём зависит только от активаций (b×s×hb \times s \times h) и связь точка-точка.

Обычно конвейеры используют на медленных связях (то есть между узлами), чтобы лучше масштабировать память.

Производительность конвейера сильно зависит от размера батча (33)

Заголовок раздела «Производительность конвейера сильно зависит от размера батча (33)»

[График: TFLOP/s на GPU при размере конвейера 1, 2, 4, 8 — при батче 128 почти не падает (около 170 → 160), при батче 8 падает с 165 до 90.] Размер батча — ключ к сокрытию пузыря, иначе конвейер быстро губит производительность.

Меняем пропускную способность на загрузку (34)

Заголовок раздела «Меняем пропускную способность на загрузку (34)»

[Две диаграммы для 4 устройств: обычное расписание и «чередующееся» (interleaved), где каждому устройству назначено несколько стадий.] Более хитрые схемы конвейера повышают загрузку, но ценой пропускной способности.

Делим обратный проход на две части:

  1. обратное распространение по активациям (z,xz, x);
  2. вычисление градиентов весов (WW).

Вторую часть можно делать когда угодно. [Рисунки из статьи Zero Bubble Pipeline Parallelism: граф вычислений MLP с разделением на F, B, W и расписания 1F1B и ZB-H1/ZB-H2, где блоки W заполняют пузыри.]

Есть ли схемы параллелизма по модели с лучшей загрузкой? Конвейерный параллелизм можно представлять как разрез вдоль глубины. А вдоль ширины?

Простое наблюдение об умножении матриц: разбиваем на подматрицы и складываем частичные суммы. [Схема: XAXA при X=[X1,X2]X = [X_1, X_2] и A=[A1;A2]A = [A_1; A_2] равно X1A1+X2A2X_1 A_1 + X_2 A_2.]

Тензорный параллелизм — у GPU свои подматрицы (37)

Заголовок раздела «Тензорный параллелизм — у GPU свои подматрицы (37)»

[Схема из Megatron-LM: блок MLP Y=GeLU(XA)Y = \mathrm{GeLU}(XA), Z=Dropout(YB)Z = \mathrm{Dropout}(YB) с A=[A1,A2]A = [A_1, A_2] (разрез по столбцам) и B=[B1;B2]B = [B_1; B_2] (разрез по строкам); до блока — оператор ff, после — оператор gg.]

Назначаем столбцы (A1A_1, A2A_2) и строки (B1B_1, B2B_2) разным GPU.

  • В прямом проходе ff — тождественное преобразование, gg — all-reduce.
  • В обратном проходе ff — all-reduce, gg — тождественное.

Когда применяют тензорный параллелизм? (38)

Заголовок раздела «Когда применяют тензорный параллелизм? (38)»

На GPU тензорный параллелизм используют внутри узла (до 8 GPU) из-за высокоскоростных соединений. [График пропускной способности на GPU для модели 3B при TP = 2, 4, 8, 16, 32: падение −10,8%, −12,2%, затем −42,7% и −65,6% — как только выходим за пределы узла.]

Тензорный параллелизм: плюсы и минусы по сравнению с конвейерным (39)

Заголовок раздела «Тензорный параллелизм: плюсы и минусы по сравнению с конвейерным (39)»

Плюсы:

  • нет пузыря — если сеть достаточно быстрая, никто никого не ждёт;
  • низкая сложность — модели легко «обернуть» без больших изменений инфраструктуры;
  • не нужен большой размер батча.

Минусы: намного больше связи, чем у конвейерного параллелизма.

  • Конвейерный: bshbsh связи точка-точка на микробатч.
  • Тензорный: 8bsh(ndevices−1ndevices)8bsh \left(\frac{n_{devices} - 1}{n_{devices}}\right) на слой, и это связь all-reduce.

Используйте тензорный параллелизм там, где есть соединения с низкой задержкой и высокой пропускной способностью.

Последняя сложность — память динамична! (40)

Заголовок раздела «Последняя сложность — память динамична! (40)»

Память — это не только статичные части, но и активации! Они бывают большими. [График потребления памяти во время шага обучения: снизу постоянный слой параметров и состояния оптимизатора, сверху — пилообразные «горбы» активаций и градиентов.]

Последняя сложность — память активаций (41)

Заголовок раздела «Последняя сложность — память активаций (41)»

До сих пор мы обсуждали только память параметров. Тензорный и конвейерный параллелизм могут уменьшить её линейно… а как же активации? [Столбчатая диаграмма из Korthikanti et al. (2022): для моделей 22B, 175B, 530B и 1T активации (зелёные) составляют большую часть памяти и превышают 80 ГБ в базовом варианте.]

Отправная точка: память активаций, если хранить всё:

Память активаций на слой=sbh(34+5ash).\text{Память активаций на слой} = sbh\left(34 + 5\frac{as}{h}\right).

Обозначения: aa — число голов внимания, bb — размер микробатча, hh — размер скрытого слоя, LL — число слоёв трансформера, pp — размер конвейерного параллелизма, ss — длина последовательности, tt — размер тензорного параллелизма, vv — размер словаря.

  • Члены 5ash5\frac{as}{h} происходят от квадратичных по длине частей внимания, включая dropout.
  • Как и во FlashAttention, этот член можно убрать пересчётом.
Память активаций на слой=sbh(10+24t+5asht).\text{Память активаций на слой} = sbh\left(10 + \frac{24}{t} + 5\frac{as}{ht}\right).

Тензорный параллелизм делит умножения матриц во внимании и MLP. Оставшийся член 10 — для LayerNorm (4sbh4sbh), Dropout (2sbh2sbh) и входов внимания и MLP (4sbh4sbh). Только эти члены продолжают расти с размером.

Делаем память по-настоящему линейной — последовательный параллелизм (44)

Заголовок раздела «Делаем память по-настоящему линейной — последовательный параллелизм (44)»

[Схема из Korthikanti et al.: в слое трансформера чередуются области «последовательного параллелизма» (LayerNorm, Dropout) и «тензорного параллелизма» (внимание, MLP); между ними — операторы gg и gˉ\bar g.]

Наблюдение: все члены 10sbh10sbh — поточечные операции по последовательности… так что разделим слои LayerNorm и dropout вдоль оси последовательности.

  • В прямом проходе gg — all-gather, gˉ\bar g — reduce-scatter.
  • В обратном проходе — наоборот.

Память активаций полностью масштабируется с числом машин (45)

Заголовок раздела «Память активаций полностью масштабируется с числом машин (45)»

Собираем всё вместе, чтобы память масштабировалась линейно:

КонфигурацияПамять активаций на слой трансформера
без параллелизмаsbh(34+5ash)sbh\left(34 + 5\frac{as}{h}\right)
тензорный параллелизм (базовый)sbh(10+24t+5asht)sbh\left(10 + \frac{24}{t} + 5\frac{as}{ht}\right)
тензорный + последовательныйsbh(34t+5asht)sbh\left(\frac{34}{t} + 5\frac{as}{ht}\right)
тензорный + выборочный пересчёт активацийsbh(10+24t)sbh\left(10 + \frac{24}{t}\right)
тензорный + последовательный + выборочный пересчётsbh(34t)sbh\left(\frac{34}{t}\right)
  • Контекстный параллелизм / кольцевое внимание (ring attention) — делим активации длинной последовательности между GPU. [Схема: блоки ключей и значений передаются по кольцу между устройствами.]
  • Экспертный параллелизм (expert parallelпараллелизм экспертовEN expert parallelism (EP)Разные эксперты MoE-слоя живут на разных GPU; токены пересылаются к своим экспертам и обратно через all-to-all-обмен.) — делим экспертов между GPU. [Схемы расположения экспертов MoE по устройствам.]

Для чего хорош каждый примитив?

Накладные расходы синхронизацииПамятьПропускная способностьРазмер батчаПростота
DDP / ZeRO-1на батчне масштабируется2 × #paramsлинейноочень просто
FSDP (ZeRO-3)3× на блок FSDPлинейно3 × #paramsлинейноочень просто
Конвейерныйна конвейерлинейноактивациилинейнонет
Тензорный + последовательный2× на блок трансформералинейно8 × активации, all-reduce на слойне влияетнет

Приходится балансировать ограниченные ресурсы — память, пропускную способность, размер батча.

Параллелизм по модели против тензорного (книга о TPU) (48)

Заголовок раздела «Параллелизм по модели против тензорного (книга о TPU) (48)»

Ключевая величина — глобальный размер батча (делённый на число GPU). [График из книги «How To Scale Your Model»: отношение времени FLOPs ко времени связи в зависимости от батча на чип для стратегий DP, FSDP, MP и FSDP + MP; при малом батче не работает ни одна схема, при среднем — только смешанные, при большом — и FSDP.]

Простые эмпирические правила из литературы.

  1. Пока модель не поместится в память:
    • тензорный параллелизм — до числа GPU в машине;
    • конвейерный параллелизм — между машинами;
    • (или ZeRO-3, в зависимости от пропускной способности).
  2. Затем, пока не кончатся GPU:
    • остаток масштабируем параллелизмом по данным.

Если размер батча маленький… используйте накопление градиентов, чтобы обменять большие батчи на лучшую эффективность связи.

Стратегии масштабирования из Narayanan et al. (2021) (50)

Заголовок раздела «Стратегии масштабирования из Narayanan et al. (2021) (50)»

[Таблица из статьи «Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM»: модели от 1,7 до 1008 млрд параметров; размер тензорного параллелизма 1 → 8, конвейерного 1 → 64, число GPU от 32 до 3072, достигнутые 44–52% пиковых FLOPS.]

Заметки:

  • тензорный параллелизм растёт первым до 8, затем останавливается на 8;
  • конвейерный параллелизм растёт, чтобы модель поместилась;
  • параллелизм по данным постепенно уменьшается с масштабом, у самой большой модели DP = 6.

Аккуратный «3D»-параллелизм даёт линейный выигрыш (51)

Заголовок раздела «Аккуратный «3D»-параллелизм даёт линейный выигрыш (51)»

[Тот же график из Megatron-LM: TFLOP/s на GPU у PTD-P не падает при росте числа GPU.] Больше GPU — та же, ровная загрузка!

Тензорный параллелизм = 8 часто оптимален (52)

Заголовок раздела «Тензорный параллелизм = 8 часто оптимален (52)»

[График: пропускная способность на GPU для конфигураций (конвейер, тензор) = (2, 32), (4, 16), (8, 8), (16, 4), (32, 2) при батче 32 и 128; пик — у (8, 8).] При распараллеливании на 64 машины лучше всего конфигурация 8 × 8.

Пересчёт активаций может окупиться (через память) (53)

Заголовок раздела «Пересчёт активаций может окупиться (через память) (53)»

[График: пропускная способность (последовательностей в секунду) от размера батча с пересчётом активаций и без: без пересчёта батч ограничен 8 и пропускная способность не превышает ~4; с пересчётом батч растёт до 256, пропускная способность — до ~8.] Пересчёт активаций позволяет брать большие батчи и повышает пропускную способность (t=8t = 8, p=16p = 16).

[Фрагмент статьи OLMo о распределённом обучении: ZeRO через FSDP в PyTorch, смешанная точность.] Dolma (OLMo) — модель 7B, FSDP (вероятно, помещается в один узел).

[Фрагмент статьи DeepSeek LLM о фреймворке HAI-LLM: параллелизм по данным, тензорный, последовательный и конвейерный 1F1B, как в Megatron; ZeRO-1 для шардинга состояния оптимизатора.]

  • DeepSeek — ZeRO-1 с тензорным, последовательным и конвейерным параллелизмом.
  • V3 — PP (16), EP (64, 8 узлов), ZeRO-1.
  • Yi — ZeRO-1 + тензорный + конвейерный параллелизм.
  • Yi-Lightning (2025) — тензорный заменён экспертным параллелизмом.

[Таблица из статьи Llama 3: 8192 GPU, TP = 8, CP = 1, PP = 16, DP = 64, длина 8192 — 430 TFLOPs на GPU, MFU 43%; 16 384 GPU, DP = 128 — 400 TFLOPs, 41%; 16 384 GPU, CP = 16, DP = 8, длина 131 072 — 380 TFLOPs, 38%.] (Этап 1 — обучение с малым батчем, этап 2 — претрейн, этап 3 — длинный контекст.)

[Цитата из статьи: порядок измерений [TP, CP, PP, DP] выбран под сеть — самому внутреннему (TP) нужны максимальная пропускная способность и минимальная задержка, поэтому он обычно внутри одного сервера; самый внешний (DP, то есть FSDP) терпит большую задержку, потому что асинхронно подгружает шардированные веса и сводит градиенты.]

Отступление: на таком масштабе — множество отказов GPU! [Таблица причин неожиданных прерываний за 54 дня претрейна: неисправный GPU — 148 (30,1%), память HBM3 — 72 (17,2%), программные ошибки — 54 (12,9%), сетевые коммутаторы и кабели — 35 (8,4%), внеплановое обслуживание хостов — 32 (7,6%), память SRAM GPU — 19 (4,5%) и т. д.; около 78% прерываний связаны с железом.]

Для моделей 2B, 9B, 27B: ZeRO-3, MP (= TP + SP), DP. [Фрагмент техотчёта: обучение на TPUv4/v5e/v5p, например, для 27B — конфигурация 8 × 24 × 32 из 6144 чипов с 768-кратной репликацией данных и 8-кратным шардингом модели.]

  • Масштабирование за определённый предел требует параллелизма на многих GPU и многих узлах.
  • Единого решения проблемы параллелизма нет (скорее всего, нужны все три подхода).
  • Есть простые, понятные эмпирические правила сочетания разных форм параллелизма.

На прошлой неделе: параллелизм внутри одного GPU. На этой неделе: параллелизм между несколькими GPU. [Рисунок: обзор узла с несколькими GPU.]

В обоих случаях вычисления (арифметико-логические блоки) далеко от входов/выходов (данных). Объединяющая тема: организовать вычисления так, чтобы избежать узких мест в передаче данных.

  • Прошлая неделя: уменьшаем обращения к памяти через слияние и тайлинг.
  • Эта неделя: уменьшаем связь между GPU/узлами через репликацию и шардинг.

Обобщённая иерархия (от маленького и быстрого к большому и медленному):

  • один узел, один GPU: кэш L1 / разделяемая память;
  • один узел, один GPU: HBM;
  • один узел, много GPU: NVLink;
  • много узлов, много GPU: NVSwitch.

Эта лекция: воплотить понятия прошлой лекции в коде.

Часть 1. Строительные блоки распределённой связи и вычислений

Заголовок раздела «Часть 1. Строительные блоки распределённой связи и вычислений»

Коллективные операции — концептуальные примитивы распределённого программирования (Википедия).

  • «Коллективные» значит, что вы задаёте шаблон связи сразу для многих (например, 256) узлов.
  • Это классика литературы по параллельному программированию с 1980-х.
  • Лучшая и более быстрая абстракция, чем вручную управлять связью точка-точка.

Терминология:

  • World size (размер мира) — число устройств (например, 4).
  • Rank (ранг) — номер устройства (например, 0, 1, 2, 3).

[Рисунки из учебника PyTorch для каждой операции.]

  • Broadcast — тензор ранга-источника копируется всем.
  • Scatter — тензор ранга-источника режется на части, каждый ранг получает свою.
  • Gather — части со всех рангов собираются на одном.
  • Reduce — тензоры всех рангов суммируются (или min, max) на одном.
  • All-gather — части со всех рангов собираются на всех.
  • Reduce-scatter — тензоры суммируются, и каждый ранг получает свою часть суммы.
  • All-reduce = reduce-scatter + all-gather.

Как запомнить термины:

  • Reduce выполняет какую-то ассоциативную и коммутативную операцию (sum, min, max).
  • Broadcast/scatter — обратные к gather.
  • All означает, что получатели — все устройства.

Классика (дома):

  • GPU в одном узле общаются через шину PCI(e) (версия 7.0, 16 линий ⇒ 242 ГБ/с);
  • GPU в разных узлах общаются через Ethernet (около 200 МБ/с).

Современность (в дата-центре):

  • внутри узла: NVLink соединяет GPU напрямую, минуя CPU;
  • между узлами: NVSwitch соединяет GPU напрямую, минуя Ethernet.

У каждого H100 18 линий NVLink 4.0, всего 900 ГБ/с. Для сравнения, пропускная способность памяти HBM — 3,9 ТБ/с.

Проверим, какое у нас железо: nvidia-smi topo -m.

GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 NIC0 NIC1 ...
GPU0 X NV18 NV18 NV18 NV18 NV18 NV18 NV18 PIX NODE ...
GPU1 NV18 X NV18 NV18 NV18 NV18 NV18 NV18 NODE PIX ...
...
Legend:
SYS = путь через PCIe и межсокетную связь NUMA-узлов (QPI/UPI)
NODE = путь через PCIe и связь между корневыми мостами PCIe внутри NUMA-узла
PHB = путь через PCIe и корневой мост PCIe (обычно CPU)
PXB = путь через несколько мостов PCIe (минуя корневой мост)
PIX = путь не более чем через один мост PCIe
NV# = путь через связку из # линий NVLink

Заметьте: GPU соединены через NV18, а также подключены к сетевым картам (NIC) через PCIe.

NCCLNCCLEN NCCL (NVIDIA Collective Communications Library)Библиотека NVIDIA для коллективных операций между GPU (all-reduce, all-gather, reduce-scatter): сама выбирает путь — NVLink, PCIe напрямую (P2P), через память CPU или по сети. Бэкенд torch.distributed для видеокарт. переводит коллективные операции в низкоуровневые пакеты, которые пересылаются между GPU (доклад):

  • определяет топологию железа (число узлов, коммутаторы, NVLink/PCIe);
  • оптимизирует путь между GPU;
  • запускает ядра CUDA для отправки и приёма данных.

Документация.

  • Даёт чистый интерфейс к коллективным операциям (например, all_gather_into_tensor).
  • Поддерживает несколько бэкендов для разного железа: gloo (CPU), nccl (GPU).
  • Поддерживает и алгоритмы более высокого уровня (например, FullyShardedDataParallel) [в этом курсе не используем].

Разберём примеры.

def collective_operations_main(rank: int, world_size: int):
"""Эта функция выполняется асинхронно в каждом процессе (rank = 0, ..., world_size - 1)."""
setup(rank, world_size)
# All-reduce
dist.barrier() # Ждём, пока все процессы дойдут до этой точки (здесь — ради печати)
tensor = torch.tensor([0., 1, 2, 3], device=get_device(rank)) + rank # И вход, и выход
print(f"Rank {rank} [before all-reduce]: {tensor}", flush=True)
dist.all_reduce(tensor=tensor, op=dist.ReduceOp.SUM, async_op=False) # Меняет tensor на месте
print(f"Rank {rank} [after all-reduce]: {tensor}", flush=True)
# Reduce-scatter
dist.barrier()
input = torch.arange(world_size, dtype=torch.float32, device=get_device(rank)) + rank # Вход
output = torch.empty(1, device=get_device(rank)) # Выделяем выход
print(f"Rank {rank} [before reduce-scatter]: input = {input}, output = {output}", flush=True)
dist.reduce_scatter_tensor(output=output, input=input, op=dist.ReduceOp.SUM, async_op=False)
print(f"Rank {rank} [after reduce-scatter]: input = {input}, output = {output}", flush=True)
# All-gather
dist.barrier()
input = output # Вход — это выход reduce-scatter
output = torch.empty(world_size, device=get_device(rank)) # Выделяем выход
print(f"Rank {rank} [before all-gather]: input = {input}, output = {output}", flush=True)
dist.all_gather_into_tensor(output_tensor=output, input_tensor=input, async_op=False)
print(f"Rank {rank} [after all-gather]: input = {input}, output = {output}", flush=True)
cleanup()
def setup(rank: int, world_size: int):
# Где живёт главный процесс (rank 0); он нужен для координации (сами данные идут через NCCL)
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "15623"
if torch.cuda.is_available():
dist.init_process_group("nccl", rank=rank, world_size=world_size)
else:
dist.init_process_group("gloo", rank=rank, world_size=world_size)
def cleanup():
torch.distributed.destroy_process_group()

Вывод (4 процесса, порядок строк перемешан, потому что процессы печатают одновременно):

Rank 0 [before all-reduce]: tensor([0., 1., 2., 3.])
Rank 1 [before all-reduce]: tensor([1., 2., 3., 4.])
Rank 2 [before all-reduce]: tensor([2., 3., 4., 5.])
Rank 3 [before all-reduce]: tensor([3., 4., 5., 6.])
Rank 0 [after all-reduce]: tensor([ 6., 10., 14., 18.]) ← у всех рангов одинаково
Rank 0 [after reduce-scatter]: input = tensor([0., 1., 2., 3.]), output = tensor([6.])
Rank 1 [after reduce-scatter]: input = tensor([1., 2., 3., 4.]), output = tensor([10.])
Rank 2 [after reduce-scatter]: input = tensor([2., 3., 4., 5.]), output = tensor([14.])
Rank 3 [after reduce-scatter]: input = tensor([3., 4., 5., 6.]), output = tensor([18.])
Rank 0 [after all-gather]: input = tensor([6.]), output = tensor([ 6., 10., 14., 18.]) ← у всех

И действительно: all-reduce = reduce-scatter + all-gather!

Посмотрим, насколько быстро идёт связь (ограничимся одним узлом).

def all_reduce(rank: int, world_size: int, num_elements: int):
setup(rank, world_size)
# Создаём тензор
tensor = torch.randn(num_elements, device=get_device(rank))
# Прогрев
dist.all_reduce(tensor=tensor, op=dist.ReduceOp.SUM, async_op=False)
if torch.cuda.is_available():
torch.cuda.synchronize() # Ждём завершения ядер CUDA
dist.barrier() # Ждём, пока все процессы дойдут сюда
# Выполняем all-reduce
start_time = time.time()
dist.all_reduce(tensor=tensor, op=dist.ReduceOp.SUM, async_op=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
dist.barrier()
end_time = time.time()
duration = end_time - start_time
print(f"[all_reduce] Rank {rank}: all_reduce(world_size={world_size}, num_elements={num_elements}) took {render_duration(duration)}", flush=True)
# Измеряем эффективную пропускную способность
dist.barrier()
size_bytes = tensor.element_size() * tensor.numel()
sent_bytes = size_bytes * 2 * (world_size - 1) # 2x: отправляем вход и получаем выход
total_duration = world_size * duration
bandwidth = sent_bytes / total_duration
print(f"[all_reduce] Rank {rank}: all_reduce measured bandwidth = {round(bandwidth / 1024**3)} GB/s", flush=True)
cleanup()
def reduce_scatter(rank: int, world_size: int, num_elements: int):
setup(rank, world_size)
# Создаём вход и выход
input = torch.randn(world_size, num_elements, device=get_device(rank)) # У каждого ранга — матрица
output = torch.empty(num_elements, device=get_device(rank))
# Прогрев
dist.reduce_scatter_tensor(output=output, input=input, op=dist.ReduceOp.SUM, async_op=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
dist.barrier()
# Выполняем reduce-scatter
start_time = time.time()
dist.reduce_scatter_tensor(output=output, input=input, op=dist.ReduceOp.SUM, async_op=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
dist.barrier()
end_time = time.time()
duration = end_time - start_time
print(f"[reduce_scatter] Rank {rank}: reduce_scatter(world_size={world_size}, num_elements={num_elements}) took {render_duration(duration)}", flush=True)
# Измеряем эффективную пропускную способность
dist.barrier()
data_bytes = input.element_size() * input.numel() # Сколько данных во входе
sent_bytes = data_bytes * (world_size - 1) # Сколько нужно отправить (здесь без 2x)
total_duration = world_size * duration # Общее время передачи
bandwidth = sent_bytes / total_duration
print(f"[reduce_scatter] Rank {rank}: reduce_scatter measured bandwidth = {round(bandwidth / 1024**3)} GB/s", flush=True)
cleanup()
spawn(all_reduce, world_size=4, num_elements=100 * 1024**2)
spawn(reduce_scatter, world_size=4, num_elements=100 * 1024**2)
[all_reduce] Rank 0: all_reduce(world_size=4, num_elements=104857600) took 2.14ms
[all_reduce] Rank 0: all_reduce measured bandwidth = 273 GB/s (у рангов 272–278 GB/s)
[reduce_scatter] Rank 0: reduce_scatter(world_size=4, num_elements=104857600) took 4.06ms
[reduce_scatter] Rank 0: reduce_scatter measured bandwidth = 72 GB/s (у рангов 72–75 GB/s)

Справка: как рассуждать об операциях (документация nccl-tests), пример кода (Стас Бекман, ml-engineering).

Разберём «голые» реализации каждой стратегии на глубоких MLP. Напомним, MLP — вычислительное узкое место трансформеров, так что пример показателен.

def generate_sample_data():
batch_size = 128
num_dim = 1024
data = torch.randn(batch_size, num_dim)
return data
def get_init_params(num_inputs: int, num_outputs: int, rank: int) -> nn.Parameter:
torch.random.manual_seed(0) # Для воспроизводимости: на всех рангах одни и те же веса
return nn.Parameter(torch.randn(num_inputs, num_outputs, device=get_device(rank)) / math.sqrt(num_outputs))

[Рисунок: батч режется вдоль оси примеров.] Стратегия шардинга: каждый ранг получает свою часть данных.

def data_parallelism_main(rank: int, world_size: int, data: torch.Tensor, num_layers: int, num_steps: int):
setup(rank, world_size)
# Берём часть данных для этого ранга (на практике каждый ранг должен загружать только свои данные)
batch_size = data.size(0) # → 128
num_dim = data.size(1) # → 1024
local_batch_size = int_divide(batch_size, world_size) # → 32
start_index = rank * local_batch_size # → 0, 32, 64, 96 (у рангов 0..3)
end_index = start_index + local_batch_size
data = data[start_index:end_index].to(get_device(rank))
# Создаём параметры MLP params[0], ..., params[num_layers - 1] (у каждого ранга — все параметры)
params = [get_init_params(num_dim, num_dim, rank) for i in range(num_layers)]
optimizer = torch.optim.AdamW(params, lr=1e-3) # У каждого ранга своё состояние оптимизатора
for step in range(num_steps):
# Прямой проход
x = data
for param in params:
x = x @ param
x = F.gelu(x)
loss = x.square().mean() # Функция потерь — средний квадрат величины
# Обратный проход
loss.backward()
# Синхронизируем градиенты между процессами (единственное отличие обычного обучения от DDP)
for param in params:
dist.all_reduce(tensor=param.grad, op=dist.ReduceOp.AVG, async_op=False)
# Обновляем параметры
optimizer.step()
print(f"[data_parallelism] Rank {rank}: step = {step}, loss = {loss.item()}, params = {[summarize_tensor(params[i]) for i in range(num_layers)]}", flush=True)
cleanup()
spawn(data_parallelism_main, world_size=4, data=data, num_layers=4, num_steps=1)
[data_parallelism] Rank 1: step = 0, loss = 0.013036300428211689, params = ['1024x1024[-0.0299...]', '1024x1024[-0.0299...]', '1024x1024[-0.0279...]', '1024x1024[-0.0299...]']
[data_parallelism] Rank 3: step = 0, loss = 0.01319716777652502, params = ['1024x1024[-0.0299...]', ...]
[data_parallelism] Rank 2: step = 0, loss = 0.012783264741301537, params = ['1024x1024[-0.0299...]', ...]
[data_parallelism] Rank 0: step = 0, loss = 0.013143327087163925, params = ['1024x1024[-0.0299...]', ...]

Заметки:

  • потери у рангов разные (посчитаны на локальных данных);
  • градиенты сводятся all-reduce и становятся одинаковыми у всех рангов;
  • поэтому и параметры у всех рангов остаются одинаковыми.

[Рисунок: каждый слой режется вдоль ширины.] Стратегия шардинга: каждый ранг получает часть каждого слоя, передаём все данные/активации.

def tensor_parallelism_main(rank: int, world_size: int, data: torch.Tensor, num_layers: int):
setup(rank, world_size)
data = data.to(get_device(rank))
batch_size = data.size(0) # → 128
num_dim = data.size(1) # → 1024
local_num_dim = int_divide(num_dim, world_size) # Шардируем num_dim → 256
# Создаём модель (каждый ранг получает 1/world_size параметров)
params = [get_init_params(num_dim, local_num_dim, rank) for i in range(num_layers)]
# Прямой проход
x = data
for i in range(num_layers):
# Считаем активации (batch_size x local_num_dim)
x = x @ params[i] # Заметьте: только на части параметров
x = F.gelu(x)
# Выделяем память под активации (world_size x batch_size x local_num_dim)
activations = [torch.empty(batch_size, local_num_dim, device=get_device(rank)) for _ in range(world_size)]
# Пересылаем активации через all-gather
dist.all_gather(tensor_list=activations, tensor=x, async_op=False)
# Склеиваем, получаем batch_size x num_dim
x = torch.cat(activations, dim=1)
print(f"[tensor_parallelism] Rank {rank}: forward pass produced activations {summarize_tensor(x)}", flush=True)
# Обратный проход: домашнее упражнение
cleanup()
[tensor_parallelism] Rank 1: forward pass produced activations 128x1024[0.5497...]
[tensor_parallelism] Rank 3: forward pass produced activations 128x1024[0.5497...]
[tensor_parallelism] Rank 0: forward pass produced activations 128x1024[0.5497...]
[tensor_parallelism] Rank 2: forward pass produced activations 128x1024[0.5497...]

[Рисунок: модель режется вдоль глубины.] Стратегия шардинга: каждый ранг получает подмножество слоёв, передаём все данные/активации.

def pipeline_parallelism_main(rank: int, world_size: int, data: torch.Tensor, num_layers: int, num_micro_batches: int):
setup(rank, world_size)
# Используем все данные
data = data.to(get_device(rank))
batch_size = data.size(0) # → 128
num_dim = data.size(1) # → 1024
# Делим слои
local_num_layers = int_divide(num_layers, world_size) # → 2
# Каждый ранг получает подмножество слоёв
local_params = [get_init_params(num_dim, num_dim, rank) for i in range(local_num_layers)]
# Прямой проход
# Разбиваем на микробатчи, чтобы уменьшить пузырь
micro_batch_size = int_divide(batch_size, num_micro_batches) # → 32
if rank == 0:
# Данные
micro_batches = data.chunk(chunks=num_micro_batches, dim=0)
else:
# Выделяем память под активации
micro_batches = [torch.empty(micro_batch_size, num_dim, device=get_device(rank)) for _ in range(num_micro_batches)]
for x in micro_batches:
# Получаем активации от предыдущего ранга
if rank - 1 >= 0:
dist.recv(tensor=x, src=rank - 1)
# Считаем слои, назначенные этому рангу
for param in local_params:
x = x @ param
x = F.gelu(x)
# Отправляем следующему рангу
if rank + 1 < world_size:
print(f"[pipeline_parallelism] Rank {rank}: sending {summarize_tensor(x)} to rank {rank + 1}", flush=True)
dist.send(tensor=x, dst=rank + 1)
# Обратный проход: домашнее упражнение
cleanup()
spawn(pipeline_parallelism_main, world_size=2, data=data, num_layers=4, num_micro_batches=4)
[pipeline_parallelism] Rank 0: sending 32x1024[-0.0603...] to rank 1
[pipeline_parallelism] Rank 0: sending 32x1024[-0.1507...] to rank 1
[pipeline_parallelism] Rank 0: sending 32x1024[-0.1025...] to rank 1
[pipeline_parallelism] Rank 0: sending 32x1024[0.1981...] to rank 1

Не реализовано: перекрытие связи и вычислений, чтобы убрать пузыри конвейера.

  • Более общих моделей (с вниманием и т. д.).
  • Большего перекрытия связи и вычислений.
  • Для этого нужен более сложный код с бо́льшим объёмом учёта.
  • Jax/TPU: достаточно задать модель и стратегию шардинга, остальное сделает компилятор Jax (levanter).
  • Но мы работаем в PyTorch, чтобы вы видели, как всё строится из примитивов.
  • Распараллеливать можно многими способами: по данным (батч), тензорный/экспертный (ширина), конвейерный (глубина), последовательный (длина).
  • Можно пересчитать, хранить в памяти или хранить в памяти другого GPU и пересылать.
  • Железо становится быстрее, но всегда будут нужны модели больше, так что эта иерархическая структура останется.

  1. Коллективные операции на CPU. Запустите collective_operations_main с бэкендом gloo (без GPU) для 4 процессов и убедитесь, что all-reduce = reduce-scatter + all-gather. Затем то же на 4 GPU с NCCL.
  2. Бенчмарк связи на своей машине. Запустите all_reduce и reduce_scatter на 4 × RTX 4080 для 100 М элементов. Формула лекции — это шинная скорость nccl-tests; сравните своё число с 273 ГБ/с у H100 с NVLink. Во сколько раз связь медленнее? Что это значит для частоты синхронизации?
  3. Обратный проход для тензорного и конвейерного параллелизма — домашнее упражнение из лекции. Для тензорного: какая коллективная операция нужна вместо all-gather в обратном проходе (подсказка: слайд 37 лекции 7)?
  4. Посчитайте память. Для модели 1,5 млрд параметров с AdamW (12 байт на параметр, как на слайде 25) посчитайте память на карту при DDP, ZeRO-1, ZeRO-2 и ZeRO-3 на 4 картах. Какой вариант поместится в 32 ГБ с запасом на активации?
ENRU
collective operations: broadcast, scatter, gather, reduce, all-gather, reduce-scatter, all-reduceколлективные операции (оставляем английские названия)
world size, rankразмер мира (число процессов), ранг (номер процесса)
NVLink, NVSwitch, PCIe, InfiniBandбыстрые связи GPU–GPU внутри узла и между узлами; PCIe — общая шина
data parallelism (DDP)параллелизм по данным
ZeRO stage 1/2/3, FSDPшардинг состояния оптимизатора / градиентов / параметров
master weightsглавные веса (FP32-копия для обновлений)
model parallelismпараллелизм по модели
pipeline parallelism, micro-batch, bubbleконвейерный параллелизм, микробатч, пузырь (простой)
tensor parallelismтензорный параллелизм
sequence parallelism, context parallelism, ring attentionпоследовательный, контекстный параллелизм, кольцевое внимание
expert parallelismэкспертный параллелизм
activation recomputation (checkpointing)пересчёт активаций
3D parallelism3D-параллелизм (данные × тензор × конвейер)
overlap communication and computationперекрытие связи и вычислений
gradient accumulationнакопление градиентов