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.
Лекция 7. Основы параллелизма
Заголовок раздела «Лекция 7. Основы параллелизма»План и цели (2)
Заголовок раздела «План и цели (2)»- Понять системные сложности обучения огромных моделей.
- Разные парадигмы распараллеливания и почему их используют несколько сразу.
- Как обычно выглядят большие запуски обучения.
Что сегодня (3)
Заголовок раздела «Что сегодня (3)»- Часть 1: основы сетей для LLM.
- Часть 2: разные формы параллельного обучения LLM.
- Часть 3: масштабирование и обучение больших LM с параллелизмом.
Пределы масштабирования на GPU — вычисления (4)
Заголовок раздела «Пределы масштабирования на GPU — вычисления (4)»У масштабирования на одном GPU есть пределы. [Слева — слайд Билла Далли о росте производительности одного чипа в 1000 раз; справа — график роста производительности суперкомпьютеров от мегафлопсов в 1990-х до экзафлопсов в 2020-х.] У самых быстрых суперкомпьютеров мира — экзафлопсы вычислений.
Пределы масштабирования на GPU — память (5)
Заголовок раздела «Пределы масштабирования на GPU — память (5)»Модели становятся очень большими… [График размера моделей по годам в логарифмической шкале: 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 — различия на уровне связи (9)
Заголовок раздела «TPU и GPU — различия на уровне связи (9)»- Сеть TPU: тороидальная сетка (каждый чип связан с соседями, края сетки замкнуты в кольцо).
- Сеть GPU: «все со всеми» (до 256 GPU). [Схемы DGX A100 SuperPOD и H100 SuperPOD с полностью связанными коммутаторами NVLink и таблица пропускной способности.]
Итоги части 1 (10)
Заголовок раздела «Итоги части 1 (10)»- Новая единица вычислений — дата-центр.
- Чего мы хотим от масштабирования на много машин:
- линейного масштабирования памяти (максимальное число параметров растёт с числом GPU);
- линейного масштабирования вычислений (FLOPs модели растут линейно с числом GPU).
- Простых примитивов коллективной коммуникации.
Часть 2. Стандартные примитивы распараллеливания LLM (11)
Заголовок раздела «Часть 2. Стандартные примитивы распараллеливания LLM (11)»Как распараллелить LLM? Три важные идеи:
- Параллелизм по данным (data parallelism):
- наивный параллелизм по данным;
- уровни ZeRO 1–3.
- Параллелизм по модели (model parallelism):
- конвейерный параллелизм (pipeline parallel);
- тензорный параллелизм (tensor parallel).
- Параллелизм по активациям:
- последовательный параллелизм (sequence parallel).
Наивный параллелизм по данным (12)
Заголовок раздела «Наивный параллелизм по данным (12)»Отправная точка: представим, что мы делаем обычный SGD:
Наивный параллелизм: делим примеров батча между машинами. Обмениваемся градиентами, чтобы синхронизироваться.
Как это работает?
- Масштабирование вычислений — каждый GPU получает примеров.
- Накладные расходы на связь — каждый батч передаётся 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: на GPU, модель млрд параметров, байт состояния оптимизатора на параметр. Базовый вариант: = 120 ГБ на GPU. (шардинг состояния оптимизатора): = 31,4 ГБ. (плюс градиенты): = 16,6 ГБ. (плюс параметры): = 1,9 ГБ.]
ZeRO, этап 1: шардинг состояния оптимизатора (16)
Заголовок раздела «ZeRO, этап 1: шардинг состояния оптимизатора (16)»Идея:
- разделить состояние оптимизатора (первый и второй моменты) между GPU;
- у всех есть параметры и градиенты.
Каждый процесс отвечает за обновление подмножества параметров (соответствующего его доле).
ZeRO, этап 1: как это работает (17)
Заголовок раздела «ZeRO, этап 1: как это работает (17)»- Шаг 1. Каждый считает полный градиент на своей части батча.
- Шаг 2. Reduce-scatter градиентов — стоимость связи #params.
- Шаг 3. Каждая машина обновляет свои параметры, используя свой градиент и состояние.
- Шаг 4. All-gather параметров — стоимость связи #params.
Сравнение ZeRO-1 и наивного параллелизма по данным (18)
Заголовок раздела «Сравнение ZeRO-1 и наивного параллелизма по данным (18)»| Наивный DDP | ZeRO, этап 1 | |
|---|---|---|
| Примитив связи | один all-reduce (градиентов) | один reduce-scatter (отправка градиентов) + all-gather (сбор параметров) |
| Стоимость связи | 2 × #params | 2 × #params |
| Память | #params | #params |
ZeRO-1 — это бесплатный (в режиме, ограниченном пропускной способностью) выигрыш в памяти.
ZeRO, этап 2: простое расширение — шардинг градиентов (19)
Заголовок раздела «ZeRO, этап 2: простое расширение — шардинг градиентов (19)»Воодушевлённые успехом, шардируем ещё больше. [: = 16,6 ГБ.]
Идея:
- градиенты (розовые доли) тоже хранить разделёнными между машинами;
- использовать (примерно) те же приёмы, что в этапе 1.
Сложность: полный вектор градиентов нельзя создать целиком ни разу, но каждый процесс должен посчитать полный градиент (ведь это параллелизм по данным).
ZeRO, этап 2: как это работает (20)
Заголовок раздела «ZeRO, этап 2: как это работает (20)»- Шаг 1. Каждый постепенно идёт назад по графу вычислений.
- Шаг 1а. Посчитав градиенты слоя, сразу делаем reduce, чтобы отправить их нужному процессу.
- Шаг 1б. Как только градиенты больше не нужны в обратном графе, сразу освобождаем их.
- Шаг 2. Каждая машина обновляет свои параметры, используя свой градиент и состояние.
- Шаг 3. All-gather параметров.
ZeRO, этап 3 (он же FSDP): шардируем всё (21)
Заголовок раздела «ZeRO, этап 3 (он же FSDP): шардируем всё (21)»[: = 1,9 ГБ.] До сих пор почти всё доставалось бесплатно… попробуем решить все проблемы с памятью.
Идея:
- шардировать всё — включая параметры!
- использовать те же идеи «постепенной коммуникации и вычислений»;
- отправлять и запрашивать параметры по требованию, проходя по графу вычислений.
Можно ли сделать это с малыми накладными расходами?
ZeRO-3 (FSDP): как это работает (упрощённо) (22)
Заголовок раздела «ZeRO-3 (FSDP): как это работает (упрощённо) (22)»[Схема из учебника PyTorch FSDP: для каждого блока модели — all-gather весов, локальный прямой проход, освобождение полных весов; в обратном проходе — снова all-gather, локальный обратный проход, reduce-scatter градиентов, освобождение весов; в конце — локальное обновление.]
Стоимость связи — 2 all-gather (#params), 1 reduce-scatter (#params).
Как FSDP / ZeRO-3 работает на самом деле (23)
Заголовок раздела «Как FSDP / ZeRO-3 работает на самом деле (23)»Разберём пример FSDP, чтобы увидеть важные идеи. [Временная диаграмма из статьи о PyTorch FSDP: на CPU ставятся в очередь all-gather (AG) и вычисления; в отдельном потоке связи AG0, AG1, AG2 идут параллельно с прямым проходом FWD0, FWD1, FWD2; в обратном проходе reduce-scatter (RS) перекрываются с BWD.]
- Постепенные вычисления и связь: параметры и градиенты запрашиваются/отправляются и сразу освобождаются.
- Перекрытие связи и вычислений (пример ): все all-gather запускаются сразу, пока идёт прямой проход, — это маскирует стоимость связи.
В чём смысл? (24)
Заголовок раздела «В чём смысл? (24)»Распределённый параллелизм по данным стоит 2 × #params на связь. А ZeRO?
- ZeRO-1 — 2 × #params, это бесплатно, так что его можно всегда включать.
- ZeRO-2 — 2 × #params, это (почти) бесплатно (если не считать накладных расходов).
- ZeRO-3 — 3 × #params: в 1,5 раза дороже по связи, но это неплохо! (если не считать задержки…)
К тому же это концептуально очень просто — пишешь обёртку для блока FSDP.
ZeRO на практике: поместится ли? (25)
Заголовок раздела «ZeRO на практике: поместится ли? (25)»Чистое обучение в BF16 (с суммированием Кэхэна) работает, и состояния оптимизатора не такие тяжёлые. Пусть всё в BF16, кроме главных весов, — 12 байт на параметр. На 8 × A100 80 ГБ:
| Макс. размер (параметров) | Формула байт на параметр | |
|---|---|---|
| Базовый | 6,66… млрд | 12 |
| ZeRO-1 | 16 млрд | 5 |
| ZeRO-2 | 24,62 млрд | 2 (параметры) + 10 (градиенты + состояние) / 8 |
| ZeRO-3 | 53,33 млрд | 12 / 8 |
Остающиеся проблемы параллелизма по данным — вычисления (26)
Заголовок раздела «Остающиеся проблемы параллелизма по данным — вычисления (26)»При параллелизме по данным число машин меньше размера батча (а около этой границы накладные расходы на связь высоки). И у роста батча убывающая отдача. [График «Predicted Training Speed» из работы о критическом размере батча: при малом батче — «идеальное масштабирование», после шумового масштаба — «неэффективное масштабирование».]
Остающиеся проблемы параллелизма по данным — модели не помещаются (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 пересылает параметры).
Разберём два вида параллелизма:
- Конвейерный параллелизм.
- Тензорный параллелизм (+ последовательный).
Они соответствуют двум разным способам разрезать модель.
Послойный параллелизм (29)
Заголовок раздела «Послойный параллелизм (29)»[Схема: слои 0–3 на GPU 0–3; прямой проход идёт слева направо, обратный — справа налево.] Послойный параллелизм режет модель по слоям и назначает подмножества слоёв разным GPU. Активации и частичные градиенты передаются туда и обратно.
Что не так с послойным параллелизмом (30)
Заголовок раздела «Что не так с послойным параллелизмом (30)»Загрузка при послойном параллелизме ужасна… При GPU каждый GPU активен времени. [Диаграмма-«лесенка»: F0, F1, F2, F3 выполняются по очереди, затем B3, B2, B1, B0.] Каждый GPU большую часть времени простаивает, ожидая, пока обратный проход дойдёт до него.
Решение: конвейерный параллелизм (31)
Заголовок раздела «Решение: конвейерный параллелизм (31)»[Диаграмма: батч разбит на 4 микробатча; GPU 0 считает F0,0, F0,1, F0,2, F0,3 и не ждёт, GPU 1 начинает с задержкой на один шаг и т. д.; посередине — «пузырь» простоя.]
Решение — конвейерный параллелизм. Обрабатываем «микробатчи» (здесь 4). Отправили первый микробатч — начинаем считать второй.
Отношение времени пузыря к полезным вычислениям:
так что нужен большой размер батча!
Зачем конвейерный параллелизм? (32)
Заголовок раздела «Зачем конвейерный параллелизм? (32)»Конвейеры кажутся ужасными. Зачем их используют?
- Конвейеры экономят память (по сравнению с DDP).
- У конвейеров могут быть хорошие свойства связи (по сравнению с FSDP): объём зависит только от активаций () и связь точка-точка.
Обычно конвейеры используют на медленных связях (то есть между узлами), чтобы лучше масштабировать память.
Производительность конвейера сильно зависит от размера батча (33)
Заголовок раздела «Производительность конвейера сильно зависит от размера батча (33)»[График: TFLOP/s на GPU при размере конвейера 1, 2, 4, 8 — при батче 128 почти не падает (около 170 → 160), при батче 8 падает с 165 до 90.] Размер батча — ключ к сокрытию пузыря, иначе конвейер быстро губит производительность.
Меняем пропускную способность на загрузку (34)
Заголовок раздела «Меняем пропускную способность на загрузку (34)»[Две диаграммы для 4 устройств: обычное расписание и «чередующееся» (interleaved), где каждому устройству назначено несколько стадий.] Более хитрые схемы конвейера повышают загрузку, но ценой пропускной способности.
Конвейер «без пузыря» (35)
Заголовок раздела «Конвейер «без пузыря» (35)»Делим обратный проход на две части:
- обратное распространение по активациям ();
- вычисление градиентов весов ().
Вторую часть можно делать когда угодно. [Рисунки из статьи Zero Bubble Pipeline Parallelism: граф вычислений MLP с разделением на F, B, W и расписания 1F1B и ZB-H1/ZB-H2, где блоки W заполняют пузыри.]
Параллелизм по модели вдоль ширины (36)
Заголовок раздела «Параллелизм по модели вдоль ширины (36)»Есть ли схемы параллелизма по модели с лучшей загрузкой? Конвейерный параллелизм можно представлять как разрез вдоль глубины. А вдоль ширины?
Простое наблюдение об умножении матриц: разбиваем на подматрицы и складываем частичные суммы. [Схема: при и равно .]
Тензорный параллелизм — у GPU свои подматрицы (37)
Заголовок раздела «Тензорный параллелизм — у GPU свои подматрицы (37)»[Схема из Megatron-LM: блок MLP , с (разрез по столбцам) и (разрез по строкам); до блока — оператор , после — оператор .]
Назначаем столбцы (, ) и строки (, ) разным GPU.
- В прямом проходе — тождественное преобразование, — all-reduce.
- В обратном проходе — all-reduce, — тождественное.
Когда применяют тензорный параллелизм? (38)
Заголовок раздела «Когда применяют тензорный параллелизм? (38)»На GPU тензорный параллелизм используют внутри узла (до 8 GPU) из-за высокоскоростных соединений. [График пропускной способности на GPU для модели 3B при TP = 2, 4, 8, 16, 32: падение −10,8%, −12,2%, затем −42,7% и −65,6% — как только выходим за пределы узла.]
Тензорный параллелизм: плюсы и минусы по сравнению с конвейерным (39)
Заголовок раздела «Тензорный параллелизм: плюсы и минусы по сравнению с конвейерным (39)»Плюсы:
- нет пузыря — если сеть достаточно быстрая, никто никого не ждёт;
- низкая сложность — модели легко «обернуть» без больших изменений инфраструктуры;
- не нужен большой размер батча.
Минусы: намного больше связи, чем у конвейерного параллелизма.
- Конвейерный: связи точка-точка на микробатч.
- Тензорный: на слой, и это связь all-reduce.
Используйте тензорный параллелизм там, где есть соединения с низкой задержкой и высокой пропускной способностью.
Последняя сложность — память динамична! (40)
Заголовок раздела «Последняя сложность — память динамична! (40)»Память — это не только статичные части, но и активации! Они бывают большими. [График потребления памяти во время шага обучения: снизу постоянный слой параметров и состояния оптимизатора, сверху — пилообразные «горбы» активаций и градиентов.]
Последняя сложность — память активаций (41)
Заголовок раздела «Последняя сложность — память активаций (41)»До сих пор мы обсуждали только память параметров. Тензорный и конвейерный параллелизм могут уменьшить её линейно… а как же активации? [Столбчатая диаграмма из Korthikanti et al. (2022): для моделей 22B, 175B, 530B и 1T активации (зелёные) составляют большую часть памяти и превышают 80 ГБ в базовом варианте.]
Сколько памяти активаций на слой? (42)
Заголовок раздела «Сколько памяти активаций на слой? (42)»Отправная точка: память активаций, если хранить всё:
Обозначения: — число голов внимания, — размер микробатча, — размер скрытого слоя, — число слоёв трансформера, — размер конвейерного параллелизма, — длина последовательности, — размер тензорного параллелизма, — размер словаря.
- Члены происходят от квадратичных по длине частей внимания, включая dropout.
- Как и во FlashAttention, этот член можно убрать пересчётом.
Активации при тензорном параллелизме (43)
Заголовок раздела «Активации при тензорном параллелизме (43)»Тензорный параллелизм делит умножения матриц во внимании и MLP. Оставшийся член 10 — для LayerNorm (), Dropout () и входов внимания и MLP (). Только эти члены продолжают расти с размером.
Делаем память по-настоящему линейной — последовательный параллелизм (44)
Заголовок раздела «Делаем память по-настоящему линейной — последовательный параллелизм (44)»[Схема из Korthikanti et al.: в слое трансформера чередуются области «последовательного параллелизма» (LayerNorm, Dropout) и «тензорного параллелизма» (внимание, MLP); между ними — операторы и .]
Наблюдение: все члены — поточечные операции по последовательности… так что разделим слои LayerNorm и dropout вдоль оси последовательности.
- В прямом проходе — all-gather, — reduce-scatter.
- В обратном проходе — наоборот.
Память активаций полностью масштабируется с числом машин (45)
Заголовок раздела «Память активаций полностью масштабируется с числом машин (45)»Собираем всё вместе, чтобы память масштабировалась линейно:
| Конфигурация | Память активаций на слой трансформера |
|---|---|
| без параллелизма | |
| тензорный параллелизм (базовый) | |
| тензорный + последовательный | |
| тензорный + выборочный пересчёт активаций | |
| тензорный + последовательный + выборочный пересчёт |
Другие стратегии параллелизма (46)
Заголовок раздела «Другие стратегии параллелизма (46)»- Контекстный параллелизм / кольцевое внимание (ring attention) — делим активации длинной последовательности между GPU. [Схема: блоки ключей и значений передаются по кольцу между устройствами.]
- Экспертный параллелизм (expert parallelпараллелизм экспертовEN expert parallelism (EP)Разные эксперты MoE-слоя живут на разных GPU; токены пересылаются к своим экспертам и обратно через all-to-all-обмен.) — делим экспертов между GPU. [Схемы расположения экспертов MoE по устройствам.]
Итоговая таблица параллелизма LLM (47)
Заголовок раздела «Итоговая таблица параллелизма LLM (47)»Для чего хорош каждый примитив?
| Накладные расходы синхронизации | Память | Пропускная способность | Размер батча | Простота | |
|---|---|---|---|---|---|
| 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.]
«3D-параллелизм» — собираем всё вместе (49)
Заголовок раздела ««3D-параллелизм» — собираем всё вместе (49)»Простые эмпирические правила из литературы.
- Пока модель не поместится в память:
- тензорный параллелизм — до числа GPU в машине;
- конвейерный параллелизм — между машинами;
- (или ZeRO-3, в зависимости от пропускной способности).
- Затем, пока не кончатся 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.] Пересчёт активаций позволяет брать большие батчи и повышает пропускную способность (, ).
Недавние LM — что делают они? (54)
Заголовок раздела «Недавние LM — что делают они? (54)»[Фрагмент статьи OLMo о распределённом обучении: ZeRO через FSDP в PyTorch, смешанная точность.] Dolma (OLMo) — модель 7B, FSDP (вероятно, помещается в один узел).
DeepSeek (55)
Заголовок раздела «DeepSeek (55)»[Фрагмент статьи DeepSeek LLM о фреймворке HAI-LLM: параллелизм по данным, тензорный, последовательный и конвейерный 1F1B, как в Megatron; ZeRO-1 для шардинга состояния оптимизатора.]
- DeepSeek — ZeRO-1 с тензорным, последовательным и конвейерным параллелизмом.
- V3 — PP (16), EP (64, 8 узлов), ZeRO-1.
Yi (56)
Заголовок раздела «Yi (56)»- Yi — ZeRO-1 + тензорный + конвейерный параллелизм.
- Yi-Lightning (2025) — тензорный заменён экспертным параллелизмом.
Llama 3 405B (57)
Заголовок раздела «Llama 3 405B (57)»[Таблица из статьи 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) терпит большую задержку, потому что асинхронно подгружает шардированные веса и сводит градиенты.]
Llama 3 405B (58)
Заголовок раздела «Llama 3 405B (58)»Отступление: на таком масштабе — множество отказов 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% прерываний связаны с железом.]
Gemma 2 (59)
Заголовок раздела «Gemma 2 (59)»Для моделей 2B, 9B, 27B: ZeRO-3, MP (= TP + SP), DP. [Фрагмент техотчёта: обучение на TPUv4/v5e/v5p, например, для 27B — конфигурация 8 × 24 × 32 из 6144 чипов с 768-кратной репликацией данных и 8-кратным шардингом модели.]
Итоги всей лекции (60)
Заголовок раздела «Итоги всей лекции (60)»- Масштабирование за определённый предел требует параллелизма на многих GPU и многих узлах.
- Единого решения проблемы параллелизма нет (скорее всего, нужны все три подхода).
- Есть простые, понятные эмпирические правила сочетания разных форм параллелизма.
Лекция 8. Параллелизм в коде
Заголовок раздела «Лекция 8. Параллелизм в коде»На прошлой неделе: параллелизм внутри одного 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.
NVIDIA Collective Communication Library (NCCL)
Заголовок раздела «NVIDIA Collective Communication Library (NCCL)»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 для отправки и приёма данных.
Библиотека PyTorch distributed (torch.distributed)
Заголовок раздела «Библиотека PyTorch distributed (torch.distributed)»- Даёт чистый интерфейс к коллективным операциям (например,
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).
Часть 2. Распределённое обучение
Заголовок раздела «Часть 2. Распределённое обучение»Разберём «голые» реализации каждой стратегии на глубоких 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 и пересылать.
- Железо становится быстрее, но всегда будут нужны модели больше, так что эта иерархическая структура останется.
Что сделать руками
Заголовок раздела «Что сделать руками»- Коллективные операции на CPU. Запустите
collective_operations_mainс бэкендом gloo (без GPU) для 4 процессов и убедитесь, что all-reduce = reduce-scatter + all-gather. Затем то же на 4 GPU с NCCL. - Бенчмарк связи на своей машине. Запустите
all_reduceиreduce_scatterна 4 × RTX 4080 для 100 М элементов. Формула лекции — это шинная скорость nccl-tests; сравните своё число с 273 ГБ/с у H100 с NVLink. Во сколько раз связь медленнее? Что это значит для частоты синхронизации? - Обратный проход для тензорного и конвейерного параллелизма — домашнее упражнение из лекции. Для тензорного: какая коллективная операция нужна вместо all-gather в обратном проходе (подсказка: слайд 37 лекции 7)?
- Посчитайте память. Для модели 1,5 млрд параметров с AdamW (12 байт на параметр, как на слайде 25) посчитайте память на карту при DDP, ZeRO-1, ZeRO-2 и ZeRO-3 на 4 картах. Какой вариант поместится в 32 ГБ с запасом на активации?
Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| 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 parallelism | 3D-параллелизм (данные × тензор × конвейер) |
| overlap communication and computation | перекрытие связи и вычислений |
| gradient accumulation | накопление градиентов |