Основы MoE
Как смесь экспертов даёт модели сотни миллиардов параметров при вычислениях как у модели в десятки раз меньше — роутер, top-k, активные параметры и борьба с перекосом загрузки.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Принято считать, что фактические знания модели хранятся в основном в её FFN-блоках. В стандартном блоке трансформера внимание занимает около параметров, а FFN — около , то есть две трети (это верно и для классического FFN с , и для SwiGLU с ). Хотим, чтобы модель знала больше, — увеличиваем FFN. Но тогда каждый токен проходит через все эти параметры, и вычисления растут так же быстро.
Смесь экспертов (MoE)смесь экспертов (MoE)EN mixture of experts (MoE) разрывает эту связь. Вместо одного большого FFN ставим много FFN поменьше — экспертовэксперт (в MoE)EN expertОдин из параллельных FFN-блоков MoE-слоя. Каждый токен обрабатывают лишь несколько экспертов, выбранных роутером., и для каждого токена включаем лишь несколько из них. Это разреженная активацияразреженная активацияEN sparse activationНа каждый токен работает лишь малая часть параметров модели. У DeepSeek-V3 — около 5.5% (37B активных из 671B).: параметров много, а работает на каждом токене малая часть.
Цифры DeepSeek-V3: 671 миллиард параметров всего, из них 37 миллиардов активныхактивные параметрыEN activated (active) parametersСколько параметров MoE-модели реально участвует в обработке одного токена. «A22B» в названии Qwen3-235B-A22B — 22 млрд активных из 235 млрд. на токен, около 5.5%. По закону 6ND из урока 5.1.1 стоимость обучения считается по активным параметрам: FLOPs. Плотная модель того же размера стоила бы в 18 раз дороже. Именно MoE, вместе с MLA и FP8, объясняет знаменитые 2.788 млн GPU-часов H800 на обучение V3. Для грубого сравнения: плотная Llama 3.1 405B потребовала 30.84 млн GPU-часов H100.
Как устроен MoE-слой
Заголовок раздела «Как устроен MoE-слой»MoE-слой заменяет FFN в блоке трансформера. Внимание остаётся обычным, и residual-связи тоже.
flowchart LR X["токен x"] --> R["роутер: softmax(x·W_g)"] R -->|"вес 0.73"| E1["эксперт 1 (FFN)"] R -->|"вес 0.27"| E2["эксперт 2 (FFN)"] R -.->|"не выбран"| E3["эксперт 3"] R -.->|"не выбран"| E4["эксперт 4"] X --> E1 X --> E2 E1 --> S["взвешенная сумма"] E2 --> S S --> Y["выход y"]
Роутермаршрутизатор (роутер), гейтEN router / gating network — это одна линейная матрица размера , где — число экспертов. Для токена :
- Логиты , вероятности .
- top-k маршрутизацияtop-k маршрутизацияEN top-k routingРоутер оценивает всех экспертов и отправляет токен к k лучшим; их выходы складываются с весами роутера.: выбираем экспертов с наибольшими , их множество обозначим .
- Веса выбранных перенормируем: .
- Выход: .
Пример с числами. Четыре эксперта, логиты роутера . Softmax даёт . При top-2 выбраны эксперты 1 и 2 (в нумерации с единицы), их веса после перенормировки и . Эксперты 3 и 4 на этом токене не тратят ни одного FLOP.
Варианты в разных моделях отличаются деталями. В Switch Transformer , и весом служит сама без перенормировки, иначе вес всегда был бы равен 1 и роутер не получал бы градиент от основного лосса. В Mixtral из 8. В DeepSeek-V3 вместо softmax используется сигмоида, и экспертов 256 (об этом — в следующем уроке).
И ещё одна деталь из практики: самые первые слои часто оставляют плотными, с обычным FFN. В официальном
config_671B.json DeepSeek-V3 за это отвечает поле n_dense_layers = 3 (в конфиге на Hugging Face оно называется
first_k_dense_replace): MoE стоит в 58 слоях из 61.
Немного истории
Заголовок раздела «Немного истории»| Год | Работа | Что нового |
|---|---|---|
| 1991 | Jacobs, Jordan, Nowlan, Hinton — Adaptive Mixtures of Local Experts | сама идея: «сеть-распорядитель» делит задачу между экспертами |
| 2017 | Shazeer и др. — Sparsely-Gated MoE | разреженный top-k с шумом, до 137B параметров (на LSTM) |
| 2020 | GShard (Google) | MoE в трансформере, top-2, 600B, машинный перевод |
| 2021 | Switch Transformer | top-1, до 1.6T параметров, aux loss и коэффициент ёмкости |
| 2023 | Mixtral 8x7B (Mistral AI) | открытые веса: 46.7B всего, 12.9B активных |
| 2024 | DeepSeekMoE, DeepSeek-V2/V3, Qwen-MoE | мелкие и общие эксперты, сотни экспертов — следующий урок |
Всего параметров и активных параметров
Заголовок раздела «Всего параметров и активных параметров»Посчитаем DeepSeek-V3 сами по официальному config_671B.json. Коротко вы это уже делали в
уроке 4.5.3; теперь разложим подсчёт по частям, чтобы увидеть,
где живут параметры. В конфиге 61 слой (первые 3 — плотные), ,
256 маршрутизируемых экспертов и 1 общий, на токен выбираются 8. Каждый эксперт — SwiGLU с промежуточной размерностью 2048.
# Считаем параметры DeepSeek-V3 по config_671B.json из официального репозиторияdim, layers, dense_layers = 7168, 61, 3inter, moe_inter = 18432, 2048n_routed, n_shared, k = 256, 1, 8vocab, H = 129280, 128q_lora, kv_lora, nope, rope_d, v_d = 1536, 512, 128, 64, 128
attn = (dim * q_lora + q_lora + q_lora * H * (nope + rope_d) # сжатие и восстановление Q + dim * (kv_lora + rope_d) + kv_lora # W_DKV и W_KR + kv_lora * H * (nope + v_d) # W_UK и W_UV + H * v_d * dim + 2 * dim) # W_O и две нормы слояexpert = 3 * dim * moe_inter # SwiGLU: w1, w2, w3dense_ffn = 3 * dim * intermoe_layers = layers - dense_layersembed = 2 * vocab * dim # вход и выход не связаны
total = layers * attn + dense_layers * dense_ffn + moe_layers * ((n_routed + n_shared) * expert + n_routed * dim) + embedactive = layers * attn + dense_layers * dense_ffn + moe_layers * ((k + n_shared) * expert + n_routed * dim) + embedprint(f"внимание (MLA) на слой: {attn / 1e6:.0f} M; один эксперт: {expert / 1e6:.0f} M")print(f"всего: {total / 1e9:.0f} B, активных на токен: {active / 1e9:.1f} B ({active / total:.1%})")внимание (MLA) на слой: 187 M; один эксперт: 44 Mвсего: 671 B, активных на токен: 37.6 B (5.6%)Сошлось с техотчётом. Видно, где живут параметры: 58 MoE-слоёв по 257 экспертов по 44M — это около 656B из 671B.
Отсюда главный компромисс MoE: вычисления — как у маленькой модели, память — как у большой. Все 671B параметров должны лежать в памяти, ведь следующий токен может выбрать любых экспертов. Для облачного сервиса с тысячами запросов это выгодно. Дома всё иначе: Qwen3-30B-A3B генерирует быстро, как модель на 3B, но требует памяти как модель на 30B (в 4-битном квантовании это 15–19 ГБ в зависимости от формата: ровно 4 бита на вес дают байта ≈ 15 ГБ, а популярный формат llama.cpp Q4_K_M хранит около 4.9 бита на вес — около 19 ГБ, см. урок 7.1.1; плюс KV-кэш).
Проблема балансировки
Заголовок раздела «Проблема балансировки»Роутер обучается вместе с моделью, и у этого процесса есть неприятная положительная обратная связь:
- Эксперт A случайно получил чуть больше токенов в начале обучения.
- Он получил больше градиентов и стал лучше остальных.
- Роутер замечает, что A снижает loss, и отправляет к нему ещё больше токенов.
Итог — коллапс маршрутизацииколлапс маршрутизацииEN routing collapseРоутер отправляет почти все токены к нескольким экспертам, остальные не обучаются. Механизм «богатые богатеют» — выбранный эксперт учится лучше и выбирается ещё чаще.: несколько экспертов перегружены, остальные почти не обучаются. Последствий три.
- Бесполезные параметры. Мёртвый эксперт занимает память, но ничего не делает.
- Медленное обучение. При параллелизме экспертов (следующий урок) эксперты живут на разных GPU. Шаг длится столько, сколько работает самая загруженная GPU, остальные ждут.
- Потерянные токены. Чтобы перегруженная GPU не тормозила всех, эксперту ставят лимит ёмкости. Его задаёт коэффициент ёмкостикоэффициент ёмкостиEN capacity factorСколько токенов может принять эксперт относительно идеально ровной раскладки. Лишние токены отбрасываются (token dropping) и проходят только по residual-связи. CF: эксперт принимает не больше токенов, где — его доля при идеальном балансе. В Switch Transformer изучали CF от 1.0 до 2.0. Лишние токены отбрасываются (token dropping) и проходят слой только по residual-связи.
Вспомогательный лосс Switch Transformer
Заголовок раздела «Вспомогательный лосс Switch Transformer»Самое распространённое лекарство — добавить к основному лоссу вспомогательный лосс балансировкивспомогательный лосс балансировки (aux loss)EN auxiliary load-balancing lossДобавка к основному лоссу, штрафующая неравномерную загрузку экспертов. Классическая форма из Switch Transformer — α·N·Σ f_i·P_i., штраф за неравномерность. В Switch Transformer он выглядит так:
- — доля токенов батча, отправленных к эксперту (после top-k).
- — средняя по батчу вероятность, которую роутер даёт эксперту .
- — коэффициент из статьи Switch Transformer.
Почему именно произведение. Самый очевидный штраф, , был бы бесполезен: получается подсчётом после top-k, и градиент через него не течёт. — гладкая функция весов роутера. Градиент : чем больше эксперт перегружен, тем сильнее лосс давит вниз его вероятность. Получается «налог на популярность».
Шкала значений. Если загрузка и вероятности равномерны (), то . При полном коллапсе на одного эксперта (, ) значение приближается к . Пример для : при загрузке и вероятностях получаем .
Есть и другие приёмы. Shazeer в 2017 году добавлял к логитам роутера перед top-k шум с обучаемой амплитудой, чтобы токены «пробовали» разных экспертов. В ST-MoE (Google, 2022) появился router z-loss, который штрафует слишком большие логиты роутера ради численной стабильности.
Практика
Заголовок раздела «Практика»Задание в браузере
Заголовок раздела «Задание в браузере»Реализуйте top-k роутинг, подсчёт загрузки экспертов, aux loss и сам MoE-слой с линейными экспертами. Тесты проверят пример из урока, крайние случаи (равномерный роутер и полный коллапс) и то, что при MoE превращается в плотную смесь всех экспертов.
import numpy as np
def softmax(x):
"""Готовый помощник: устойчивый softmax по последней оси."""
e = np.exp(x - x.max(axis=-1, keepdims=True))
return e / e.sum(axis=-1, keepdims=True)
def topk_route(logits, k):
"""Top-k роутинг.
logits: (T, E) — логиты роутера для T токенов и E экспертов.
Возвращает (idx, weights):
idx — (T, k) int: номера выбранных экспертов по убыванию вероятности
(при равенстве — меньший номер раньше: np.argsort(-p, kind='stable'));
weights — (T, k): вероятности softmax выбранных экспертов, перенормированные к сумме 1.
"""
# TODO
...
def expert_load(idx, n_experts):
"""Доля назначений, пришедшихся на каждого эксперта: f_i = count_i / (T * k). Сумма = 1."""
# TODO (подсказка: np.bincount(..., minlength=n_experts))
...
def aux_loss(logits, idx, n_experts):
"""Вспомогательный лосс балансировки в стиле Switch Transformer (без множителя alpha):
L = N * sum_i f_i * P_i,
где f_i — expert_load, P_i — средняя по токенам вероятность роутера для эксперта i.
При идеальном балансе L = 1, при полном коллапсе на одного эксперта L -> N.
"""
# TODO
...
def moe_layer(x, W_g, experts, k):
"""MoE-слой: y_t = sum_j weights[t, j] * experts[idx[t, j]](x_t).
x: (T, d); W_g: (d, E) — матрица роутера; experts — список из E матриц (d, d),
эксперт e — линейное отображение x -> x @ experts[e]. Возвращает (T, d).
"""
# TODO: логиты = x @ W_g; topk_route; сложите взвешенные выходы выбранных экспертов
...
# Проверьте себя кнопкой «Запустить»:
logits = np.array([[2.0, 1.0, 0.5, -1.0]])
res = topk_route(logits, 2)
print(res) # ожидается idx [[0 1]] и веса ≈ [[0.731 0.269]]
rng = np.random.default_rng(0)
L = rng.normal(size=(1000, 8))
res = topk_route(L, 2)
if res is not None:
load = expert_load(res[0], 8)
print("загрузка:", None if load is None else np.round(load, 3))
print("aux loss:", aux_loss(L, res[0], 8))
Эксперимент на PyTorch: коллапс и его лечение
Заголовок раздела «Эксперимент на PyTorch: коллапс и его лечение»Маленький MoE-слой учится на игрушечной задаче: 8 кластеров входов, у каждого своё линейное отображение. У всех токенов есть общая компонента, как у настоящих скрытых состояний LLM, и из-за неё роутеру легко «слипнуться» на нескольких экспертах. Обучаем с разными и смотрим на загрузку.
import torchimport torch.nn as nnimport torch.nn.functional as F
torch.set_num_threads(1) # маленькие тензоры: один поток быстрее
class MoE(nn.Module): """Классический MoE-слой: softmax-роутер, top-k, aux loss в стиле Switch."""
def __init__(self, d, d_ff, n_experts=8, k=2): super().__init__() self.k, self.E = k, n_experts self.router = nn.Linear(d, n_experts, bias=False) self.experts = nn.ModuleList( nn.Sequential(nn.Linear(d, d_ff), nn.GELU(), nn.Linear(d_ff, d)) for _ in range(n_experts) )
def forward(self, x): # x: (T, d) — токены уже «расплющены» probs = self.router(x).softmax(dim=-1) # (T, E) w, idx = probs.topk(self.k, dim=-1) # (T, k) if self.k > 1: # при k=1 (Switch) вес — сама p_i, w = w / w.sum(dim=-1, keepdim=True) # иначе роутер не получит градиент y = torch.zeros_like(x) for e, expert in enumerate(self.experts): rows, slot = torch.where(idx == e) # кто пришёл к эксперту e if rows.numel(): # эксперт считает только свои токены x[rows] # index_add_(0, rows, src): прибавить строки src к строкам y с номерами rows (взвешенная сумма) y.index_add_(0, rows, w[rows, slot, None] * expert(x[rows])) f = torch.bincount(idx.flatten(), minlength=self.E).float() / idx.numel() # доля назначений P = probs.mean(dim=0) # средняя вероятность (дифференцируема) self.aux = self.E * (f * P).sum() self.load = f return y
def run(alpha, steps=200, seed=0): """Игрушечная задача: 8 кластеров, у каждого своё линейное отображение. Возвращает MSE, aux, загрузку.""" torch.manual_seed(seed) d, n_clusters = 16, 8 centers = torch.randn(n_clusters, d) common = 2.0 * torch.randn(d) # общая компонента у всех токенов, как в реальных скрытых состояниях maps = torch.randn(n_clusters, d, d) / d ** 0.5 moe = MoE(d, 32, n_experts=8, k=2) opt = torch.optim.Adam(moe.parameters(), lr=3e-3) for _ in range(steps): c = torch.randint(0, n_clusters, (256,)) x = centers[c] + common + 0.3 * torch.randn(256, d) x = x / x.pow(2).mean(-1, keepdim=True).sqrt() # RMSNorm, как перед FFN в трансформере target = torch.einsum("td,tde->te", x, maps[c]) # каждый токен × матрица своего кластера loss = F.mse_loss(moe(x), target) + alpha * moe.aux opt.zero_grad() loss.backward() opt.step() return F.mse_loss(moe(x), target).item(), moe.aux.item(), moe.load
for alpha in (0.0, 0.01, 0.1): mse, aux, load = run(alpha) print(f"alpha={alpha:<4} MSE={mse:.3f} aux={aux:.2f} загрузка={[round(v, 2) for v in load.tolist()]}")Вывод на CPU (PyTorch 2.x, от нескольких секунд до полуминуты на три прогона в зависимости от процессора; на другой версии PyTorch числа могут немного отличаться):
alpha=0.0 MSE=0.032 aux=1.62 загрузка=[0.0, 0.26, 0.0, 0.0, 0.22, 0.05, 0.41, 0.07]alpha=0.01 MSE=0.028 aux=1.03 загрузка=[0.18, 0.14, 0.07, 0.09, 0.14, 0.06, 0.19, 0.12]alpha=0.1 MSE=0.027 aux=1.01 загрузка=[0.15, 0.08, 0.1, 0.13, 0.14, 0.12, 0.13, 0.14]Без балансировки три эксперта из восьми мертвы, а один забирает 41% назначений. С работают все,
и качество даже немного лучше: живых экспертов больше, значит, больше параметров реально используется.
Попробуйте top-1: замените k=2 на k=1 в строке moe = MoE(...). При коллапс в этой задаче
становится полным при любом seed: вся нагрузка уходит одному эксперту, а aux подбирается к .
Проверь себя
Заголовок раздела «Проверь себя»- MoE заменяет FFN набором экспертов. Роутер выбирает top-k из них для каждого токена, выход — взвешенная сумма.
- Активные параметры определяют вычисления, общие — память. DeepSeek-V3: 671B всего и 37B активных.
- Без балансировки роутер склонен к коллапсу: часть экспертов умирает, обучение тормозит, токены отбрасываются.
- Классическое лекарство — aux loss из Switch Transformer, но он конфликтует с основной целью обучения.
Дальше: как DeepSeek переизобрела MoE — мелкие и общие эксперты и балансировка без вспомогательного лосса. Урок про DeepSeekMoE.