DualPipe и EPLB: конвейер в обе стороны и балансировщик экспертов
Полный перевод README двух библиотек четвёртого дня Open Source Week DeepSeek — двунаправленного конвейерного параллелизма DualPipe/DualPipeV и балансировщика нагрузки экспертов EPLB — с переведённым кодом eplb.py и разбором примера, который запускается на CPU.
Часть 1. DualPipe
Заголовок раздела «Часть 1. DualPipe»Оригинал: README.md репозитория deepseek-ai/DualPipe.
DualPipe — новаторский алгоритм двунаправленного конвейерного параллелизма, представленный в техническом отчёте DeepSeek-V3. Он добивается полного наложения фаз вычислений и обмена данными при прямом и обратном проходах, а заодно уменьшает пузыри конвейера. Подробнее о наложении вычислений и обмена данными см. данные профилирования.
Расписания
Заголовок раздела «Расписания»Пример расписания DualPipe для 8 рангов конвейера (PP ranks) и 20 микробатчей, идущих в двух направлениях. Микробатчи обратного направления симметричны микробатчам прямого, поэтому для простоты их номера на рисунке не показаны. Две клетки в общей чёрной рамке — это вычисления и обмен данными, наложенные друг на друга.
flowchart LR subgraph D0["GPU 0"] S0a["стадия 1"] S0b["стадия 8"] end subgraph D1["GPU 1"] S1a["стадия 2"] S1b["стадия 7"] end subgraph D3["GPU 7"] S3a["стадия 8"] S3b["стадия 1"] end A["микробатчи направления А"] --> S0a S0a --> S1a S1a -.-> S3a B["микробатчи направления Б"] --> S3b S3b -.-> S1b S1b --> S0b
DualPipeV
Заголовок раздела «DualPipeV»DualPipeV — компактное V-образное расписание, полученное из DualPipe процедурой «разрезания пополам». Его предложила Sea AI Lab под названием «Cut-in-half» в своей заметке в блоге. Спасибо им за это эффективное расписание!
Расписания
Заголовок раздела «Расписания»Пример расписания DualPipeV для 4 рангов конвейера (8 стадий конвейера) и 10 микробатчей.
Сравнение пузырей конвейера и расхода памяти (при одинаковом числе стадий PP)
Заголовок раздела «Сравнение пузырей конвейера и расхода памяти (при одинаковом числе стадий PP)»| Метод | Пузырь | Параметры на устройство | Активации на устройство | Число устройств |
|---|---|---|---|---|
| 1F1B | 1× | |||
| ZB1P | 1× | |||
| DualPipe | 2× | |||
| DualPipeV | 2× |
— число стадий конвейера (чётное). — время выполнения прямого чанка, — время выполнения полного обратного чанка, — время выполнения чанка «обратный проход по весам», — время выполнения двух наложенных друг на друга прямого и обратного чанков.
Быстрый старт
Заголовок раздела «Быстрый старт»Использование показано в примерах:
python examples/example_dualpipe.pypython examples/example_dualpipev.pyПримечание: в реальных приложениях вам нужно реализовать собственный метод overlapped_forward_backward,
подогнанный под ваш модуль.
Требования
Заголовок раздела «Требования»- PyTorch 2.0 и выше
Разработчики
Заголовок раздела «Разработчики»DualPipe создали и разработали Цзяши Ли (Jiashi Li), Чэнци Дэн (Chengqi Deng) и Вэньфэн Лян (Wenfeng Liang).
Цитирование
Заголовок раздела «Цитирование»@misc{deepseekai2025deepseekv3technicalreport, title={DeepSeek-V3 Technical Report}, author={DeepSeek-AI}, year={2025}, eprint={2412.19437}, archivePrefix={arXiv}, primaryClass={cs.CL}, url={https://arxiv.org/abs/2412.19437},}Часть 2. EPLB — балансировщик нагрузки для параллелизма экспертов
Заголовок раздела «Часть 2. EPLB — балансировщик нагрузки для параллелизма экспертов»Оригинал: README.md репозитория deepseek-ai/EPLB.
При параллелизме экспертов (expert parallelism, EP) разные эксперты назначаются на разные GPU. Нагрузка на экспертов меняется в зависимости от текущих запросов, поэтому важно поддерживать нагрузку на GPU сбалансированной. Как описано в статье о DeepSeek-V3, мы применяем стратегию избыточных экспертов: дублируем сильно нагруженных экспертов. Затем эвристически раскладываем дубликаты по GPU, чтобы нагрузка между GPU была сбалансирована. Кроме того, благодаря маршрутизации экспертов с ограничением по группам (group-limited expert routing), которая используется в DeepSeek-V3, мы по возможности стараемся класть экспертов одной группы на один узел, чтобы уменьшить трафик между узлами.
Чтобы облегчить воспроизведение и развёртывание, мы открываем код нашего боевого алгоритма балансировки EP в файле
eplb.py. Алгоритм вычисляет сбалансированный план копирования и размещения экспертов на основе оценок их нагрузки.
Заметим, что точный способ предсказывать нагрузку экспертов выходит за рамки этого репозитория. Распространённый
способ — скользящее среднее по историческим статистикам.
Алгоритм
Заголовок раздела «Алгоритм»У алгоритма балансировки две политики для разных случаев.
Иерархическая балансировка
Заголовок раздела «Иерархическая балансировка»Когда число серверных узлов делит число групп экспертов, мы используем иерархическую политику балансировки, чтобы воспользоваться маршрутизацией с ограничением по группам. Сначала мы равномерно раскладываем группы экспертов по узлам, так чтобы нагрузки узлов были сбалансированы. Затем копируем экспертов внутри каждого узла. Наконец, раскладываем скопированных экспертов по отдельным GPU, чтобы нагрузка на разные GPU была сбалансирована. Иерархическую политику можно использовать на стадии prefill с меньшим размером параллелизма экспертов.
Глобальная балансировка
Заголовок раздела «Глобальная балансировка»В остальных случаях мы используем глобальную политику балансировки: она копирует экспертов глобально, не обращая внимания на группы, и раскладывает копии по отдельным GPU. Эту политику можно применять на стадии decode с большим размером параллелизма экспертов.
flowchart TD
L["Нагрузка логических экспертов (скользящее среднее)"] --> Q{"Число групп делится на число узлов?"}
Q -- "да" --> H1["1. Группы → узлы (поровну по нагрузке)"]
H1 --> H2["2. Копии экспертов внутри узла"]
H2 --> H3["3. Копии → GPU узла (поровну по нагрузке)"]
Q -- "нет" --> G1["Все эксперты = одна группа на одном «узле»"]
G1 --> H2
H3 --> R["phy2log, log2phy, logcnt"]Интерфейс и пример
Заголовок раздела «Интерфейс и пример»Главная функция балансировщика — eplb.rebalance_experts.
Код ниже — пример для двухслойной MoE-модели, в каждом слое которой 12 экспертов. Мы добавляем по 4 избыточных эксперта на слой, и все 16 копий размещаются на 2 узлах по 4 GPU в каждом.
import torchimport eplb
# Нагрузка экспертов: 2 слоя × 12 экспертов (сколько токенов досталось каждому)weight = torch.tensor([[ 90, 132, 40, 61, 104, 165, 39, 4, 73, 56, 183, 86], [ 20, 107, 104, 64, 19, 197, 187, 157, 172, 86, 16, 27]])
num_replicas = 16 # физических экспертов на слой: 12 логических + 4 избыточныхnum_groups = 4 # группы экспертов (по 3 эксперта)num_nodes = 2 # серверные узлыnum_gpus = 8 # всего GPU (по 4 на узел)
phy2log, log2phy, logcnt = eplb.rebalance_experts(weight, num_replicas, num_groups, num_nodes, num_gpus)print(phy2log)
# Вывод:# tensor([[ 5, 6, 5, 7, 8, 4, 3, 4, 10, 9, 10, 2, 0, 1, 11, 1],# [ 7, 10, 6, 8, 6, 11, 8, 9, 2, 4, 5, 1, 5, 0, 3, 1]])Этот вывод получен иерархической политикой балансировки и задаёт следующий план копирования и размещения экспертов.
Лицензия
Заголовок раздела «Лицензия»Этот репозиторий кода выпущен под лицензией MIT.
Приложение: код eplb.py с переведёнными комментариями
Заголовок раздела «Приложение: код eplb.py с переведёнными комментариями»from typing import Tuple
import torch
def balanced_packing(weight: torch.Tensor, num_packs: int) -> Tuple[torch.Tensor, torch.Tensor]: """ Разложить n объектов с весами по m пакетам так, чтобы в каждом пакете было ровно n/m объектов, а суммарные веса пакетов были как можно ближе друг к другу.
Параметры: weight: [X, n], вес каждого объекта num_packs: число пакетов
Возвращает: pack_index: [X, n], номер пакета для каждого объекта rank_in_pack: [X, n], номер объекта внутри его пакета """ num_layers, num_groups = weight.shape assert num_groups % num_packs == 0 groups_per_pack = num_groups // num_packs
if groups_per_pack == 1: pack_index = torch.arange(weight.size(-1), dtype=torch.int64, device=weight.device).expand(weight.shape) rank_in_pack = torch.zeros_like(weight, dtype=torch.int64) return pack_index, rank_in_pack
indices = weight.float().sort(-1, descending=True).indices.cpu() pack_index = torch.full_like(weight, fill_value=-1, dtype=torch.int64, device='cpu') rank_in_pack = torch.full_like(pack_index, fill_value=-1) for i in range(num_layers): pack_weights = [0] * num_packs pack_items = [0] * num_packs for group in indices[i]: pack = min((i for i in range(num_packs) if pack_items[i] < groups_per_pack), key=pack_weights.__getitem__) assert pack_items[pack] < groups_per_pack pack_index[i, group] = pack rank_in_pack[i, group] = pack_items[pack] pack_weights[pack] += weight[i, group] pack_items[pack] += 1 return pack_index, rank_in_pack
def replicate_experts(weight: torch.Tensor, num_phy: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """ Размножить `num_log` экспертов до `num_phy` копий так, чтобы максимальная нагрузка среди всех копий была как можно меньше.
Параметры: weight: [X, num_log] num_phy: общее число экспертов после копирования
Возвращает: phy2log: [X, num_phy], номер логического эксперта для каждого физического rank: [X, num_phy], номер копии logcnt: [X, num_log], число копий каждого логического эксперта """ n, num_log = weight.shape num_redundant = num_phy - num_log assert num_redundant >= 0 device = weight.device phy2log = torch.arange(num_phy, dtype=torch.int64, device=device).repeat(n, 1) rank = torch.zeros(n, num_phy, dtype=torch.int64, device=device) logcnt = torch.ones(n, num_log, dtype=torch.int64, device=device) arangen = torch.arange(n, dtype=torch.int64, device=device) for i in range(num_log, num_phy): redundant_indices = (weight / logcnt).max(dim=-1).indices phy2log[:, i] = redundant_indices rank[:, i] = logcnt[arangen, redundant_indices] logcnt[arangen, redundant_indices] += 1 return phy2log, rank, logcnt
def rebalance_experts_hierarchical(weight: torch.Tensor, num_physical_experts: int, num_groups: int, num_nodes: int, num_gpus: int): """ Параметры: weight: [num_moe_layers, num_logical_experts] num_physical_experts: число физических экспертов после копирования num_groups: число групп экспертов num_nodes: число серверных узлов; сеть внутри узла (например, NVLink) быстрее, чем между узлами num_gpus: число GPU, должно быть кратно `num_nodes`
Возвращает: physical_to_logical_map: [num_moe_layers, num_physical_experts] logical_to_physical_map: [num_moe_layers, num_logical_experts, X] logical_count: [num_moe_layers, num_logical_experts] """ num_layers, num_logical_experts = weight.shape assert num_logical_experts % num_groups == 0 group_size = num_logical_experts // num_groups assert num_groups % num_nodes == 0 groups_per_node = num_groups // num_nodes assert num_gpus % num_nodes == 0 assert num_physical_experts % num_gpus == 0 phy_experts_per_gpu = num_physical_experts // num_gpus
def inverse(perm: torch.Tensor) -> torch.Tensor: inv = torch.empty_like(perm) inv.scatter_(1, perm, torch.arange(perm.size(1), dtype=torch.int64, device=perm.device).expand(perm.shape)) return inv
# Шаг 1: разложить группы по узлам tokens_per_group = weight.unflatten(-1, (num_groups, group_size)).sum(-1) group_pack_index, group_rank_in_pack = balanced_packing(tokens_per_group, num_nodes) log2mlog = (((group_pack_index * groups_per_node + group_rank_in_pack) * group_size).unsqueeze(-1) + torch.arange(group_size, dtype=torch.int64, device=group_pack_index.device)).flatten(-2) mlog2log = inverse(log2mlog)
# Шаг 2: создать избыточных экспертов внутри узлов # [num_layers * num_nodes, num_logical_experts // num_nodes] tokens_per_mlog = weight.gather(-1, mlog2log).view(-1, num_logical_experts // num_nodes) phy2mlog, phyrank, mlogcnt = replicate_experts(tokens_per_mlog, num_physical_experts // num_nodes)
# Шаг 3: разложить физических экспертов по GPU # [num_layers * num_nodes, num_physical_experts // num_nodes] tokens_per_phy = (tokens_per_mlog / mlogcnt).gather(-1, phy2mlog) pack_index, rank_in_pack = balanced_packing(tokens_per_phy, num_gpus // num_nodes) phy2pphy = pack_index * phy_experts_per_gpu + rank_in_pack pphy2phy = inverse(phy2pphy)
pphy2mlog = phy2mlog.gather(-1, pphy2phy) # [num_layers * num_nodes, num_log_per_nodes] pphy2mlog = (pphy2mlog.view(num_layers, num_nodes, -1) + torch.arange(0, num_logical_experts, num_logical_experts // num_nodes, device=group_pack_index.device).view(1, -1, 1)).flatten(-2) pphy2log = mlog2log.gather(-1, pphy2mlog) pphyrank = phyrank.gather(-1, pphy2phy).view(num_layers, -1) logcnt = mlogcnt.view(num_layers, -1).gather(-1, log2mlog) return pphy2log, pphyrank, logcnt
def rebalance_experts(weight: torch.Tensor, num_replicas: int, num_groups: int, num_nodes: int, num_gpus: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """ Точка входа балансировщика нагрузки для параллелизма экспертов.
Параметры: weight: [layers, num_logical_experts], статистика нагрузки всех логических экспертов num_replicas: число физических экспертов, должно быть кратно `num_gpus` num_groups: число групп экспертов num_nodes: число серверных узлов; сеть внутри узла (например, NVLink) быстрее, чем между узлами num_gpus: число GPU, должно быть кратно `num_nodes`
Возвращает: physical_to_logical_map: [layers, num_replicas], номер эксперта для каждой копии logical_to_physical_map: [layers, num_logical_experts, X], номера копий каждого эксперта expert_count: [layers, num_logical_experts], число физических копий каждого логического эксперта """ num_layers, num_logical_experts = weight.shape weight = weight.float().cpu() if num_groups % num_nodes == 0: # иерархическая политика балансировки phy2log, phyrank, logcnt = rebalance_experts_hierarchical(weight, num_replicas, num_groups, num_nodes, num_gpus) else: # глобальная политика балансировки phy2log, phyrank, logcnt = rebalance_experts_hierarchical(weight, num_replicas, 1, 1, num_gpus) maxlogcnt = logcnt.max().item() log2phy: torch.Tensor = torch.full((num_layers, num_logical_experts, maxlogcnt), -1, dtype=torch.int64, device=logcnt.device) log2phy.view(num_layers, -1).scatter_(-1, phy2log * maxlogcnt + phyrank, torch.arange(num_replicas, dtype=torch.int64, device=log2phy.device).expand(num_layers, -1)) return phy2log, log2phy, logcnt
__all__ = ['rebalance_experts']Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| pipeline parallelism (PP), PP rank / stage | конвейерный параллелизм, ранг / стадия конвейера |
| micro-batch | микробатч |
| pipeline bubble | пузырь конвейера (простой GPU) |
| forward / backward chunk | прямой / обратный чанк (кусок работы стадии над микробатчем) |
| backward for input / backward for weights | обратный проход по входу / по весам |
| computation-communication overlap | наложение вычислений и обмена данными |
| 1F1B, ZB1P (Zero Bubble) | расписания «один прямой — один обратный» и «без пузыря» |
| expert parallelism (EP) | параллелизм экспертов |
| redundant experts | избыточные (дублированные) эксперты |
| logical / physical expert | логический эксперт / физическая копия |
| group-limited expert routing | маршрутизация с ограничением по группам |
| hierarchical / global load balancing | иерархическая / глобальная балансировка |
| prefilling / decoding stage | стадия prefill / стадия decode |