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

DualPipe и EPLB: конвейер в обе стороны и балансировщик экспертов

Полный перевод README двух библиотек четвёртого дня Open Source Week DeepSeek — двунаправленного конвейерного параллелизма DualPipe/DualPipeV и балансировщика нагрузки экспертов EPLB — с переведённым кодом eplb.py и разбором примера, который запускается на CPU.

Оригинал: README.md репозитория deepseek-ai/DualPipe.

DualPipe — новаторский алгоритм двунаправленного конвейерного параллелизма, представленный в техническом отчёте DeepSeek-V3. Он добивается полного наложения фаз вычислений и обмена данными при прямом и обратном проходах, а заодно уменьшает пузыри конвейера. Подробнее о наложении вычислений и обмена данными см. данные профилирования.

Рисунок images/dualpipe.png.

Пример расписания 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
Идея DualPipe при 8 GPU (схема переводчика): у каждой GPU две стадии модели, микробатчи входят с обоих концов конвейера

DualPipeV — компактное V-образное расписание, полученное из DualPipe процедурой «разрезания пополам». Его предложила Sea AI Lab под названием «Cut-in-half» в своей заметке в блоге. Спасибо им за это эффективное расписание!

Рисунок images/dualpipev.png.

Пример расписания DualPipeV для 4 рангов конвейера (8 стадий конвейера) и 10 микробатчей.

Сравнение пузырей конвейера и расхода памяти (при одинаковом числе стадий PP)

Заголовок раздела «Сравнение пузырей конвейера и расхода памяти (при одинаковом числе стадий PP)»
МетодПузырьПараметры на устройствоАктивации на устройствоЧисло устройств
1F1B(PP−1)(F+B)(PP-1)(F+B)1×PPPPPPPP
ZB1P(PP−1)(F+B−2W)(PP-1)(F+B-2W)1×PPPPPPPP
DualPipe(PP/2−1)(F&B+B−3W)(PP/2-1)(F\&B+B-3W)2×PP+1PP+1PPPP
DualPipeV(PP/2−1)(F&B+B−3W)(PP/2-1)(F\&B+B-3W)2×PP+1PP+1PP/2PP/2

PPPP — число стадий конвейера (чётное). FF — время выполнения прямого чанка, BB — время выполнения полного обратного чанка, WW — время выполнения чанка «обратный проход по весам», F&BF\&B — время выполнения двух наложенных друг на друга прямого и обратного чанков.

Использование показано в примерах:

Окно терминала
python examples/example_dualpipe.py
python 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 (схема переводчика по коду eplb.py): глобальная — тот же алгоритм с одной группой и одним узлом

Главная функция балансировщика — eplb.rebalance_experts.

Код ниже — пример для двухслойной MoE-модели, в каждом слое которой 12 экспертов. Мы добавляем по 4 избыточных эксперта на слой, и все 16 копий размещаются на 2 узлах по 4 GPU в каждом.

import torch
import 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]])

Этот вывод получен иерархической политикой балансировки и задаёт следующий план копирования и размещения экспертов.

Рисунок example.png.

Этот репозиторий кода выпущен под лицензией 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']
ENRU
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