VKВлад КуликовОткрытые лекции
Современные LLM7 / 20

Модуль 7. Mixture of Experts

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

Скачать файлы курса (.zip) 2588,1 КБ

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

Ноутбук 7. Смесь экспертов: от выбора до результата

Основной ноутбук модуля

Скачать .ipynb module_07_notebook_moe.ipynb · 81,4 КБ

Показать текст ноутбука

Ноутбук 7. Смесь экспертов: от выбора до результата

Проследим один пакет токенов через выбор экспертов, ограничение вместимости и взвешенную сумму. Затем отдельно посчитаем параметры и логический объём пересылки. Эксперт — это самостоятельный FFN; общие эксперты выполняются для каждого токена.

Первый запуск: выполните все ячейки по порядку и прочитайте результаты разделов 1–3. Второй проход: измените входы, сравните две потери и контроллеры, проверьте поля конфигурации. Справочный проход: вернитесь к определениям классов и отрицательным тестам.

Все обязательные вычисления работают на CPU. При первом запуске устанавливаются только отсутствующие библиотеки; после установки данные и примеры доступны без сети. Численные данные и небольшой официальный файл конфигурации встроены с SHA-256. Их происхождение описано в data/README.md модуля. Веса моделей не загружаются.

0. Данные и общие функции

Подготовим встроенные данные, проверки входов и устойчивые функции sigmoid/softmax.

Python
import hashlib
import json
import math
from dataclasses import asdict, dataclass
from numbers import Integral, Real
from typing import Any, Literal

import importlib.util, subprocess, sys
if importlib.util.find_spec("numpy") is None:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "numpy"])
import numpy as np

QWEN_CONFIG_JSON = '{\n    "architectures": [\n        "Qwen3_5MoeForConditionalGeneration"\n    ],\n    "image_token_id": 248056,\n    "model_type": "qwen3_5_moe",\n    "text_config": {\n        "attention_bias": false,\n        "attention_dropout": 0.0,\n        "attn_output_gate": true,\n        "dtype": "bfloat16",\n        "eos_token_id": 248044,\n        "full_attention_interval": 4,\n        "head_dim": 256,\n        "hidden_act": "silu",\n        "hidden_size": 2048,\n        "initializer_range": 0.02,\n        "layer_types": [\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention",\n            "linear_attention",\n            "linear_attention",\n            "linear_attention",\n            "full_attention"\n        ],\n        "linear_conv_kernel_dim": 4,\n        "linear_key_head_dim": 128,\n        "linear_num_key_heads": 16,\n        "linear_num_value_heads": 32,\n        "linear_value_head_dim": 128,\n        "max_position_embeddings": 262144,\n        "mlp_only_layers": [],\n        "model_type": "qwen3_5_moe_text",\n        "moe_intermediate_size": 512,\n        "mtp_num_hidden_layers": 1,\n        "mtp_use_dedicated_embeddings": false,\n        "num_attention_heads": 16,\n        "num_experts": 256,\n        "num_experts_per_tok": 8,\n        "num_hidden_layers": 40,\n        "num_key_value_heads": 2,\n        "rms_norm_eps": 1e-06,\n        "router_aux_loss_coef": 0.001,\n        "shared_expert_intermediate_size": 512,\n        "use_cache": true,\n        "vocab_size": 248320,\n        "mamba_ssm_dtype": "float32",\n        "rope_parameters": {\n            "mrope_interleaved": true,\n            "mrope_section": [\n                11,\n                11,\n                10\n            ],\n            "rope_type": "default",\n            "rope_theta": 10000000,\n            "partial_rotary_factor": 0.25\n        }\n    },\n    "tie_word_embeddings": false,\n    "transformers_version": "4.57.0.dev0",\n    "video_token_id": 248057,\n    "vision_config": {\n        "deepstack_visual_indexes": [],\n        "depth": 27,\n        "hidden_act": "gelu_pytorch_tanh",\n        "hidden_size": 1152,\n        "in_channels": 3,\n        "initializer_range": 0.02,\n        "intermediate_size": 4304,\n        "model_type": "qwen3_5_moe",\n        "num_heads": 16,\n        "num_position_embeddings": 2304,\n        "out_hidden_size": 2048,\n        "patch_size": 16,\n        "spatial_merge_size": 2,\n        "temporal_patch_size": 2\n    },\n    "vision_end_token_id": 248054,\n    "vision_start_token_id": 248053\n}'
QWEN_CONFIG_SHA256 = '5e4d7f74fec2f360eb9cfbfcd6ec0c4c76e684d3a11caaed259d9fd9bfbc7944'
assert hashlib.sha256(QWEN_CONFIG_JSON.encode("utf-8")).hexdigest() == QWEN_CONFIG_SHA256
QWEN_TEXT = json.loads(QWEN_CONFIG_JSON)["text_config"]
REFERENCE_JSON = '{"active_expert_law":{"E128_k4_N16":{"simulation":0.39857421875,"theory":0.3982896965679277},"E128_k4_N64":{"simulation":0.869296875,"theory":0.868915967521525},"E256_k8_N16":{"simulation":0.39908203125,"theory":0.3982896965679277},"E256_k8_N64":{"simulation":0.86845703125,"theory":0.868915967521525},"E896_k16_N16":{"simulation":0.2504827008928572,"theory":0.2504603703415571},"E896_k16_N64":{"simulation":0.6853069196428571,"theory":0.6843699099436731}},"balance":{"proportional":{"after":[0.031524658203125,0.032073974609375,0.0316162109375,0.03106689453125,0.0311279296875,0.0299072265625,0.0306396484375,0.030487060546875,0.0323486328125,0.031005859375,0.031280517578125,0.031463623046875,0.032012939453125,0.030548095703125,0.029998779296875,0.031982421875,0.031341552734375,0.030670166015625,0.031097412109375,0.031829833984375,0.03106689453125,0.03143310546875,0.032196044921875,0.03192138671875,0.03057861328125,0.03173828125,0.0313720703125,0.031005859375,0.030975341796875,0.03179931640625,0.030792236328125,0.031097412109375],"before":[0.0804443359375,0.08746337890625,0.0877685546875,0.079925537109375,0.0166015625,0.0250244140625,0.02374267578125,0.029022216796875,0.030731201171875,0.02301025390625,0.0234375,0.0252685546875,0.02923583984375,0.026336669921875,0.016143798828125,0.02020263671875,0.0311279296875,0.022613525390625,0.022796630859375,0.029205322265625,0.016387939453125,0.015655517578125,0.02044677734375,0.025482177734375,0.024139404296875,0.023834228515625,0.0242919921875,0.0242919921875,0.02203369140625,0.0283203125,0.02496337890625,0.020050048828125],"bias":[-0.03043212890625,-0.03636474609375004,-0.03786621093750004,-0.035046386718749996,0.021752929687499988,-0.0007324218750000033,0.003442382812499999,-0.004284667968750004,-0.002709960937500002,0.00487060546875,0.0048339843749999984,-0.0004028320312499989,-0.004650878906249997,-0.001684570312499999,0.017358398437499988,0.013403320312500003,-0.006701660156249992,0.0047241210937499995,0.006225585937499997,-0.004980468749999995,0.016809082031250006,0.024023437500000022,0.013293457031249997,0.0034057617187500007,0.0036987304687499984,0.0024902343750000003,0.0012451171875000001,0.0020141601562500035,0.00974121093750001,-0.0007324218749999982,0.002380371093750004,0.01087646484375],"cv_after":0.01932878458289989,"cv_before":0.6505852027323183,"mode":"proportional","rate":0.15,"uniform":0.03125},"sign":{"after":[0.03179931640625,0.035308837890625,0.031646728515625,0.02972412109375,0.030487060546875,0.02886962890625,0.03021240234375,0.031341552734375,0.0328369140625,0.03057861328125,0.0318603515625,0.03082275390625,0.032928466796875,0.030517578125,0.029632568359375,0.033050537109375,0.03094482421875,0.030364990234375,0.030914306640625,0.032379150390625,0.0318603515625,0.0316162109375,0.031219482421875,0.030731201171875,0.029998779296875,0.02984619140625,0.031585693359375,0.030853271484375,0.03106689453125,0.032684326171875,0.031219482421875,0.031097412109375],"before":[0.0804443359375,0.08746337890625,0.0877685546875,0.079925537109375,0.0166015625,0.0250244140625,0.02374267578125,0.029022216796875,0.030731201171875,0.02301025390625,0.0234375,0.0252685546875,0.02923583984375,0.026336669921875,0.016143798828125,0.02020263671875,0.0311279296875,0.022613525390625,0.022796630859375,0.029205322265625,0.016387939453125,0.015655517578125,0.02044677734375,0.025482177734375,0.024139404296875,0.023834228515625,0.0242919921875,0.0242919921875,0.02203369140625,0.0283203125,0.02496337890625,0.020050048828125],"bias":[-0.014000000000000005,-0.01800000000000001,-0.02100000000000001,-0.01900000000000001,0.037000000000000026,0.014000000000000005,0.01900000000000001,0.013000000000000005,0.014000000000000005,0.02000000000000001,0.022000000000000013,0.015000000000000006,0.013000000000000005,0.015000000000000006,0.03300000000000002,0.03200000000000002,0.009000000000000001,0.02100000000000001,0.022000000000000013,0.012000000000000004,0.035000000000000024,0.04100000000000003,0.028000000000000018,0.01800000000000001,0.01900000000000001,0.016000000000000007,0.01800000000000001,0.01800000000000001,0.026000000000000016,0.017000000000000008,0.01900000000000001,0.027000000000000017],"cv_after":0.03931118956596903,"cv_before":0.6505852027323183,"mode":"sign","rate":0.001,"uniform":0.03125}},"capacity":{"after_counts":[6,4,4,4],"before_counts":[12,4,4,4],"capacity_per_expert":6,"dropped_assignments":6,"fully_dropped_tokens":[],"logits":[[4.0,3.0,0.0,-1.0],[4.0,0.0,3.0,-1.0],[4.0,0.0,-1.0,3.0],[4.0,3.0,0.0,-1.0],[4.0,0.0,3.0,-1.0],[4.0,0.0,-1.0,3.0],[4.0,3.0,0.0,-1.0],[4.0,0.0,3.0,-1.0],[4.0,0.0,-1.0,3.0],[4.0,3.0,0.0,-1.0],[4.0,0.0,3.0,-1.0],[4.0,0.0,-1.0,3.0]],"policy":"fifo_drop_then_renormalize_survivors"},"combinations":{"C128_4":10668000,"C256_8_unconstrained_upper_bound":409663695276000,"C8_2":28},"expert_parallel":{"bytes_per_value":2,"hidden_size":7168,"round_trip_bytes":469762048,"tokens":2048,"top_k":8},"latent_moe":{"kimi_k3_hidden":7168,"kimi_k3_latent":3584,"payload_reduction":2.0,"routed_experts":896,"selected_experts":16,"shared_experts":2},"losses":{"aux_logits":[[4.0,1.0,0.0,-1.0],[3.0,1.0,0.0,-1.0],[4.0,0.0,1.0,-1.0],[3.0,0.0,1.0,-1.0]],"switch_aux":3.522646252388999,"z_loss":13.363987745409077},"models":{"deepseek_v3_hf_package_total":685000000000,"deepseek_v3_official_active":37000000000,"deepseek_v3_official_main_total":671000000000,"deepseek_v3_reconstruction":{"active_parameters":36625618432,"attention":11413547008,"dense_ffn":1189085184,"extra_active":0,"extra_total":0,"norms":881664,"routed_and_shared_experts_active":22988980224,"routed_and_shared_experts_total":656463101952,"router":106445312,"sinks":0,"total_parameters":671026419200,"vocab_active":926679040,"vocab_total":1853358080},"gqa_params_per_layer_with_bias":26550080,"mla_params_per_layer":187107328,"oss120":{"active_parameters":5132849472,"attention":955802880,"dense_ffn":0,"extra_active":0,"extra_total":0,"norms":210240,"routed_and_shared_experts_active":3584424960,"routed_and_shared_experts_total":114701598720,"router":13275648,"sinks":2304,"total_parameters":116829156672,"vocab_active":579133440,"vocab_total":1158266880},"oss20":{"active_parameters":3608307264,"attention":637201920,"dense_ffn":0,"extra_active":0,"extra_total":0,"norms":141120,"routed_and_shared_experts_active":2389616640,"routed_and_shared_experts_total":19116933120,"router":2212608,"sinks":1536,"total_parameters":20914757184,"vocab_active":579133440,"vocab_total":1158266880}},"qwen35_config":{"hidden_size":2048,"layers":40,"mtp_layers":1,"routed_experts":256,"routed_intermediate":512,"selected_experts":8,"shared_experts":1,"shared_intermediate":512},"router":{"bias":[0.0,0.0,0.0,0.35,0.0,0.0,0.0,0.0],"boundary_update":{"one_step":{"e0":0.7497747834990175,"e6":0.7296995026250039},"two_steps":{"e0":0.7310247834990176,"e6":0.7484495026250039}},"gates":[0.2694994612569363,0.25980636470427615,0.24657595902244958,0.224118215016338],"gates_with_bias":[0.18368780089939873,0.28354280527375636,0.27334461127547466,0.2594247825513703],"logits":[1.2,-0.3,2.1,0.4,1.7,-1.0,0.9,2.5],"scores":[0.7685247834990175,0.425557483188341,0.8909031788043871,0.598687660112452,0.8455347349164652,0.2689414213699951,0.7109495026250039,0.9241418199787566],"selected":[7,2,4,0],"selected_with_bias":[3,7,2,4]}}'
REFERENCE_SHA256 = 'de9ba39bbe902907f8bbae0e6bfe1a4d19028eeedc2f167e026995a4f85ddeed'
REFERENCE = json.loads(REFERENCE_JSON)
assert hashlib.sha256(REFERENCE_JSON.encode("utf-8")).hexdigest() == REFERENCE_SHA256
print('данные проверены', REFERENCE_SHA256[:16])


def _finite_array(name: str, value: Any, *, ndim: int | None = None) -> np.ndarray:
    """Return ``value`` as a finite float64 array with an optional rank contract."""

    array = np.asarray(value, dtype=np.float64)
    if ndim is not None and array.ndim != ndim:
        raise ValueError(f"{name} must have rank {ndim}; got shape {array.shape}")
    if array.size == 0:
        raise ValueError(f"{name} must be non-empty")
    if not np.all(np.isfinite(array)):
        raise ValueError(f"{name} must contain only finite values")
    return array


def _positive_int(name: str, value: Any, *, minimum: int = 1) -> int:
    """Validate an integer scalar while rejecting booleans explicitly."""

    if isinstance(value, bool) or not isinstance(value, Integral):
        raise TypeError(f"{name} must be an integer")
    result = int(value)
    if result < minimum:
        raise ValueError(f"{name} must be >= {minimum}")
    return result


def _bounded_real(
    name: str,
    value: Any,
    *,
    lower: float,
    upper: float,
    lower_inclusive: bool = True,
    upper_inclusive: bool = True,
) -> float:
    """Validate one finite real scalar against an explicit interval."""

    if isinstance(value, bool) or not isinstance(value, Real):
        raise TypeError(f"{name} must be a real scalar")
    result = float(value)
    if not math.isfinite(result):
        raise ValueError(f"{name} must be finite")
    lower_ok = result >= lower if lower_inclusive else result > lower
    upper_ok = result <= upper if upper_inclusive else result < upper
    if not (lower_ok and upper_ok):
        left = "[" if lower_inclusive else "("
        right = "]" if upper_inclusive else ")"
        raise ValueError(f"{name} must lie in {left}{lower}, {upper}{right}")
    return result


def stable_sigmoid(value: Any) -> np.ndarray:
    """Evaluate the logistic sigmoid without overflowing for large finite logits."""

    x = _finite_array("value", value)
    out = np.empty_like(x)
    nonnegative = x >= 0.0
    out[nonnegative] = 1.0 / (1.0 + np.exp(-x[nonnegative]))
    exp_x = np.exp(x[~nonnegative])
    out[~nonnegative] = exp_x / (1.0 + exp_x)
    return out


def stable_softmax(value: Any, *, axis: int = -1) -> np.ndarray:
    """Compute a finite softmax along ``axis`` with max subtraction."""

    x = _finite_array("value", value)
    if axis < -x.ndim or axis >= x.ndim:
        raise ValueError("axis is outside the array rank")
    shifted = x - np.max(x, axis=axis, keepdims=True)
    exp_shifted = np.exp(shifted)
    denominator = exp_shifted.sum(axis=axis, keepdims=True)
    if np.any(denominator <= 0.0) or not np.all(np.isfinite(denominator)):
        raise FloatingPointError("softmax normalization failed")
    return exp_shifted / denominator


def _log_sigmoid(value: np.ndarray) -> np.ndarray:
    """Return log(sigmoid(value)) in a numerically stable form."""

    return -np.logaddexp(0.0, -value)


def _deterministic_top_k(scores: np.ndarray, top_k: int) -> np.ndarray:
    """Select descending scores with lower expert index breaking exact ties."""

    token_count, expert_count = scores.shape
    indices = np.arange(expert_count)
    selected = np.empty((token_count, top_k), dtype=np.int64)
    for token in range(token_count):
        order = np.lexsort((indices, -scores[token]))
        selected[token] = order[:top_k]
    return selected


@dataclass(frozen=True)
class RouterResult:
    """Validated output of one token-choice router pass.

    Shapes are ``clean_scores:[T,E]``, ``selected_experts:[T,k]``, and
    ``gate_weights:[T,k]``. ``selection_scores`` includes the optional balancing bias;
    ``gate_weights`` never does.
    """

    clean_scores: np.ndarray
    selection_scores: np.ndarray
    selected_experts: np.ndarray
    gate_weights: np.ndarray
    expert_counts: np.ndarray


@dataclass(frozen=True)
class DispatchPlan:
    """Capacity-limited token-to-expert assignments under a deterministic FIFO policy."""

    capacity_per_expert: int
    accepted: np.ndarray
    gate_weights: np.ndarray
    expert_counts: np.ndarray
    dropped_assignments: int
    fully_dropped_tokens: tuple[int, ...]


@dataclass(frozen=True)
class LoadStatistics:
    """Fractions and imbalance diagnostics for expert assignment counts."""

    fractions: np.ndarray
    coefficient_of_variation: float
    maximum_fraction: float
    minimum_fraction: float

1. Роутер

Сначала воспроизведём выбор четырёх экспертов, затем повысим приоритет одного из них. Сравните состав выбранного множества и веса смеси: смещение влияет на первый, но веса вычисляются из чистых оценок. Отдельные проверки очень отрицательных логитов показывают, зачем нормировка выполняется в логарифмической области.

Python
def route_logits(
    logits: Any,
    *,
    top_k: int,
    scoring: Literal["sigmoid", "softmax"] = "sigmoid",
    selection_bias: Any | None = None,
) -> RouterResult:
    """Route a non-empty token batch with deterministic token-choice top-k.

    Parameters
    ----------
    logits:
        Router logits of shape ``[T,E]`` or one token of shape ``[E]``.
    top_k:
        Number of routed experts per token, constrained to ``1 <= k <= E``.
    scoring:
        ``"sigmoid"`` for independent V3-style scores or ``"softmax"`` for a
        categorical router.
    selection_bias:
        Optional vector ``[E]`` added **only** to the selection scores. Gate weights are
        normalized from the clean scores, preserving the aux-loss-free contract.

    Exact ties are broken by lower expert index so CPU, Colab, and different NumPy builds
    produce the same route.
    """

    raw = _finite_array("logits", logits)
    if raw.ndim == 1:
        raw = raw[None, :]
    if raw.ndim != 2:
        raise ValueError("logits must have shape [T,E] or [E]")
    token_count, expert_count = raw.shape
    k = _positive_int("top_k", top_k)
    if k > expert_count:
        raise ValueError("top_k cannot exceed the number of experts")
    if scoring not in {"sigmoid", "softmax"}:
        raise ValueError("scoring must be 'sigmoid' or 'softmax'")

    if selection_bias is None:
        bias = np.zeros(expert_count, dtype=np.float64)
    else:
        bias = _finite_array("selection_bias", selection_bias, ndim=1)
        if bias.shape != (expert_count,):
            raise ValueError("selection_bias must have shape [E]")

    clean_scores = stable_sigmoid(raw) if scoring == "sigmoid" else stable_softmax(raw, axis=1)
    selection_scores = clean_scores + bias[None, :]
    # Both score functions preserve rank mathematically. Ranking raw logits when
    # no bias is supplied avoids artificial ties from saturation/underflow.
    selected = _deterministic_top_k(raw if selection_bias is None else selection_scores, k)

    selected_logits = np.take_along_axis(raw, selected, axis=1)
    if scoring == "sigmoid":
        # Normalize selected sigmoid scores in log space. This remains finite even when
        # every displayed sigmoid underflows to zero at extreme negative logits.
        log_weights = _log_sigmoid(selected_logits)
        gate_weights = stable_softmax(log_weights, axis=1)
    else:
        # Re-normalizing clean softmax scores over top-k is exactly softmax of selected logits.
        gate_weights = stable_softmax(selected_logits, axis=1)

    counts = np.bincount(selected.ravel(), minlength=expert_count).astype(np.int64)
    if not np.allclose(gate_weights.sum(axis=1), 1.0, atol=1e-12):
        raise AssertionError("router gates must sum to one per token")
    return RouterResult(
        clean_scores=clean_scores,
        selection_scores=selection_scores,
        selected_experts=selected,
        gate_weights=gate_weights,
        expert_counts=counts,
    )


def expert_capacity(
    *,
    token_count: int,
    top_k: int,
    expert_count: int,
    capacity_factor: Real,
) -> int:
    """Return ``ceil(capacity_factor * T * k / E)`` assignments per expert."""

    tokens = _positive_int("token_count", token_count)
    k = _positive_int("top_k", top_k)
    experts = _positive_int("expert_count", expert_count)
    if k > experts:
        raise ValueError("top_k cannot exceed expert_count")
    factor = _bounded_real(
        "capacity_factor",
        capacity_factor,
        lower=0.0,
        upper=float("inf"),
        lower_inclusive=False,
    )
    return max(1, math.ceil(factor * tokens * k / experts))


def dispatch_with_capacity(
    router: RouterResult,
    *,
    capacity_factor: Real = 1.0,
    renormalize_kept_gates: bool = True,
) -> DispatchPlan:
    """Apply deterministic FIFO expert capacity and optionally renormalize kept gates.

    This transparent policy is intentionally simpler than production grouped dispatch. Each
    token is visited in input order and each route slot in router order. Overflow assignments
    are dropped; no hidden rerouting occurs. If ``renormalize_kept_gates`` is true, surviving
    weights are renormalized as an explicit *course policy* rather than a universal MoE rule.
    Tokens with no surviving assignment emit zero from the expert branch, remain on the
    residual path outside this educational layer, and are reported explicitly.
    """

    selected = np.asarray(router.selected_experts, dtype=np.int64)
    gates = _finite_array("router.gate_weights", router.gate_weights, ndim=2)
    if selected.ndim != 2 or selected.shape != gates.shape:
        raise ValueError("router selected_experts and gate_weights must share shape [T,k]")
    token_count, k = selected.shape
    if token_count == 0:
        raise ValueError("router output must contain at least one token")
    expert_count = int(np.asarray(router.expert_counts).size)
    if expert_count < 1 or np.any(selected < 0) or np.any(selected >= expert_count):
        raise ValueError("router contains an invalid expert index")

    capacity = expert_capacity(
        token_count=token_count,
        top_k=k,
        expert_count=expert_count,
        capacity_factor=capacity_factor,
    )
    accepted = np.zeros_like(selected, dtype=bool)
    counts = np.zeros(expert_count, dtype=np.int64)
    for token in range(token_count):
        for slot in range(k):
            expert = int(selected[token, slot])
            if counts[expert] < capacity:
                accepted[token, slot] = True
                counts[expert] += 1

    effective_gates = np.where(accepted, gates, 0.0)
    kept_mass = effective_gates.sum(axis=1, keepdims=True)
    fully_dropped = tuple(np.flatnonzero(kept_mass[:, 0] == 0.0).tolist())
    if renormalize_kept_gates:
        np.divide(
            effective_gates,
            kept_mass,
            out=effective_gates,
            where=kept_mass > 0.0,
        )
    dropped = int(accepted.size - accepted.sum())
    return DispatchPlan(
        capacity_per_expert=capacity,
        accepted=accepted,
        gate_weights=effective_gates,
        expert_counts=counts,
        dropped_assignments=dropped,
        fully_dropped_tokens=fully_dropped,
    )


def load_statistics(counts: Any) -> LoadStatistics:
    """Convert non-negative assignment counts into fractions and load CV."""

    raw = _finite_array("counts", counts, ndim=1)
    if np.any(raw < 0.0):
        raise ValueError("counts must be non-negative")
    total = float(raw.sum())
    if total <= 0.0:
        raise ValueError("at least one assignment is required")
    fractions = raw / total
    mean = float(fractions.mean())
    return LoadStatistics(
        fractions=fractions,
        coefficient_of_variation=float(fractions.std() / mean),
        maximum_fraction=float(fractions.max()),
        minimum_fraction=float(fractions.min()),
    )


def aux_free_bias_step(
    selection_bias: Any,
    counts: Any,
    *,
    rate: Real,
    mode: Literal["sign", "proportional"] = "sign",
) -> np.ndarray:
    """Update expert selection biases outside backpropagation.

    ``mode="sign"`` reproduces the main update rule in Wang et al. (2024): an
    overloaded expert moves down by one fixed step and an underloaded expert moves up.
    ``mode="proportional"`` is the smoother ablation/teaching controller used in this
    module's convergence trace. Both modify selection only; clean gate values remain
    unchanged.
    """

    bias = _finite_array("selection_bias", selection_bias, ndim=1)
    stats = load_statistics(counts)
    if stats.fractions.shape != bias.shape:
        raise ValueError("selection_bias and counts must have the same shape")
    step = _bounded_real("rate", rate, lower=0.0, upper=float("inf"), lower_inclusive=False)
    error = stats.fractions.mean() - stats.fractions
    if mode == "sign":
        delta = np.sign(error)
    elif mode == "proportional":
        delta = error
    else:
        raise ValueError("mode must be 'sign' or 'proportional'")
    return bias + step * delta


def switch_auxiliary_loss(logits: Any, *, top_k: int = 1) -> float:
    """Compute ``E * sum_i f_i P_i`` for a softmax token-choice router batch."""

    raw = _finite_array("logits", logits)
    if raw.ndim != 2:
        raise ValueError("logits must have shape [T,E]")
    routed = route_logits(raw, top_k=top_k, scoring="softmax")
    token_count, expert_count = raw.shape
    assignment_fraction = routed.expert_counts / (token_count * top_k)
    mean_probability = routed.clean_scores.mean(axis=0)
    return float(expert_count * np.dot(assignment_fraction, mean_probability))


def router_z_loss(logits: Any) -> float:
    """Compute the ST-MoE router z-loss ``mean(logsumexp(logits)^2)``."""

    raw = _finite_array("logits", logits)
    if raw.ndim != 2:
        raise ValueError("logits must have shape [T,E]")
    maximum = np.max(raw, axis=1, keepdims=True)
    log_partition = maximum[:, 0] + np.log(np.exp(raw - maximum).sum(axis=1))
    return float(np.mean(log_partition**2))


ref = REFERENCE["router"]
base = route_logits(np.array(ref["logits"]), top_k=4, scoring="sigmoid")
biased = route_logits(np.array(ref["logits"]), top_k=4, scoring="sigmoid", selection_bias=ref["bias"])
assert base.selected_experts[0].tolist() == ref["selected"]
assert np.allclose(base.gate_weights[0], ref["gates"], atol=1e-12)
assert biased.selected_experts[0].tolist() == ref["selected_with_bias"]
assert np.allclose(biased.gate_weights[0], ref["gates_with_bias"], atol=1e-12)
assert route_logits(np.zeros(4), top_k=2, scoring="softmax").selected_experts[0].tolist() == [0, 1]
extreme = route_logits([-1002., -1000., -1001.], top_k=2)
assert extreme.selected_experts.tolist() == [[1, 2]]
assert np.all(np.isfinite(extreme.gate_weights))
assert np.allclose(extreme.gate_weights, [[1/(1+np.exp(-1)), 1/(1+np.exp(1))]])
print("top-k", base.selected_experts[0].tolist(), "biased", biased.selected_experts[0].tolist())
print('роутер проверен')

2. Вместимость и переполнение

Ограничим число назначений на эксперта и примем их в порядке поступления — FIFO. Проследите, какие веса обнулились и как перенормировались оставшиеся. Если у токена не осталось ни одного назначения, его выбираемая ветвь равна нулю; это ещё не удаление токена из остаточного потока.

Python
ref = REFERENCE["capacity"]
routed = route_logits(np.array(ref["logits"]), top_k=2, scoring="softmax")
plan = dispatch_with_capacity(routed, capacity_factor=1.0, renormalize_kept_gates=True)
assert ref["policy"] == "fifo_drop_then_renormalize_survivors"
assert plan.capacity_per_expert == ref["capacity_per_expert"] == 6
assert routed.expert_counts.tolist() == ref["before_counts"]
assert plan.expert_counts.tolist() == ref["after_counts"]
assert plan.dropped_assignments == ref["dropped_assignments"] == 6
assert list(plan.fully_dropped_tokens) == ref["fully_dropped_tokens"]
assert np.allclose(plan.gate_weights.sum(axis=1), 1.0)
print("before", routed.expert_counts, "after", plan.expert_counts, "dropped", plan.dropped_assignments)
# Fully dropped routed tokens remain observable; no division by zero.
all_same = route_logits(np.tile([9., 0.], (4, 1)), top_k=1)
limited = dispatch_with_capacity(all_same, capacity_factor=0.5)
assert limited.fully_dropped_tokens == (1, 2, 3)
assert np.all(limited.gate_weights[1:] == 0)
assert limited.accepted[:, 0].tolist() == [True, False, False, False]
print('переполнение проверено')

3. Выбираемые и общие эксперты

Соберём полный слой из роутера и SwiGLU-экспертов. Для E=1, k=1 сравним результат с тем же FFN напрямую, затем проверим точное прибавление общей ветви. Эти сравнения устанавливают согласованность операций на заданных входах, а не качество обученной модели.

Python
def swiglu_expert(inputs: Any, gate: Any, up: Any, down: Any) -> np.ndarray:
    """Apply one dense SwiGLU expert to ``inputs:[T,h]`` with explicit shape checks."""

    x = _finite_array("inputs", inputs)
    if x.ndim == 1:
        x = x[None, :]
    if x.ndim != 2:
        raise ValueError("inputs must have shape [T,h] or [h]")
    w_gate = _finite_array("gate", gate, ndim=2)
    w_up = _finite_array("up", up, ndim=2)
    w_down = _finite_array("down", down, ndim=2)
    hidden = x.shape[1]
    if w_gate.shape[0] != hidden or w_up.shape != w_gate.shape:
        raise ValueError("gate and up must both have shape [h,i_exp]")
    if w_down.shape != (w_gate.shape[1], hidden):
        raise ValueError("down must have shape [i_exp,h]")
    gate_values = x @ w_gate
    up_values = x @ w_up
    return (gate_values * stable_sigmoid(gate_values) * up_values) @ w_down


@dataclass(frozen=True)
class MoEForwardResult:
    """Output plus routing metadata from the transparent educational MoE layer."""

    output: np.ndarray
    router: RouterResult
    dispatch: DispatchPlan | None


class EducationalMoE:
    """Small NumPy MoE with routed and always-on shared SwiGLU experts.

    Expert tensors use shape ``[E,h,i_exp]`` for gate/up and ``[E,i_exp,h]`` for
    down. The Python expert loop is intentionally inspectable; production code would use
    grouped GEMMs, fused dispatch, and distributed expert parallelism.
    """

    def __init__(
        self,
        router_weights: Any,
        expert_gate: Any,
        expert_up: Any,
        expert_down: Any,
        *,
        shared_gate: Any | None = None,
        shared_up: Any | None = None,
        shared_down: Any | None = None,
    ) -> None:
        """Validate and store routed/shared expert tensors without hidden casting later."""

        self.router_weights = _finite_array("router_weights", router_weights, ndim=2)
        self.expert_gate = _finite_array("expert_gate", expert_gate, ndim=3)
        self.expert_up = _finite_array("expert_up", expert_up, ndim=3)
        self.expert_down = _finite_array("expert_down", expert_down, ndim=3)
        experts, hidden, intermediate = self.expert_gate.shape
        if self.router_weights.shape != (hidden, experts):
            raise ValueError("router_weights must have shape [h,E]")
        if self.expert_up.shape != self.expert_gate.shape:
            raise ValueError("expert_up must match expert_gate")
        if self.expert_down.shape != (experts, intermediate, hidden):
            raise ValueError("expert_down must have shape [E,i_exp,h]")

        supplied = (shared_gate is not None, shared_up is not None, shared_down is not None)
        if len(set(supplied)) != 1:
            raise ValueError("shared gate/up/down must be supplied together")
        if shared_gate is None:
            self.shared_gate = np.empty((0, hidden, intermediate), dtype=np.float64)
            self.shared_up = np.empty((0, hidden, intermediate), dtype=np.float64)
            self.shared_down = np.empty((0, intermediate, hidden), dtype=np.float64)
        else:
            self.shared_gate = _finite_array("shared_gate", shared_gate, ndim=3)
            self.shared_up = _finite_array("shared_up", shared_up, ndim=3)
            self.shared_down = _finite_array("shared_down", shared_down, ndim=3)
            shared = self.shared_gate.shape[0]
            if self.shared_gate.shape[1:] != (hidden, intermediate):
                raise ValueError("shared_gate must have shape [S,h,i_exp]")
            if self.shared_up.shape != self.shared_gate.shape:
                raise ValueError("shared_up must match shared_gate")
            if self.shared_down.shape != (shared, intermediate, hidden):
                raise ValueError("shared_down must have shape [S,i_exp,h]")

    def forward(
        self,
        inputs: Any,
        *,
        top_k: int,
        scoring: Literal["sigmoid", "softmax"] = "sigmoid",
        selection_bias: Any | None = None,
        capacity_factor: Real | None = None,
    ) -> MoEForwardResult:
        """Route and combine one batch; capacity overflow follows ``dispatch_with_capacity``."""

        x = _finite_array("inputs", inputs)
        if x.ndim == 1:
            x = x[None, :]
        if x.ndim != 2 or x.shape[1] != self.router_weights.shape[0]:
            raise ValueError("inputs must have shape [T,h]")
        router = route_logits(
            x @ self.router_weights,
            top_k=top_k,
            scoring=scoring,
            selection_bias=selection_bias,
        )
        dispatch = (
            dispatch_with_capacity(router, capacity_factor=capacity_factor)
            if capacity_factor is not None
            else None
        )
        gates = dispatch.gate_weights if dispatch is not None else router.gate_weights
        accepted = dispatch.accepted if dispatch is not None else np.ones_like(gates, dtype=bool)
        output = np.zeros_like(x)

        for expert in range(self.expert_gate.shape[0]):
            token_rows, slots = np.nonzero((router.selected_experts == expert) & accepted)
            if token_rows.size:
                expert_output = swiglu_expert(
                    x[token_rows],
                    self.expert_gate[expert],
                    self.expert_up[expert],
                    self.expert_down[expert],
                )
                output[token_rows] += gates[token_rows, slots, None] * expert_output

        for shared in range(self.shared_gate.shape[0]):
            output += swiglu_expert(
                x,
                self.shared_gate[shared],
                self.shared_up[shared],
                self.shared_down[shared],
            )
        return MoEForwardResult(output=output, router=router, dispatch=dispatch)


rng = np.random.default_rng(60)
hidden, intermediate = 8, 12
x = rng.standard_normal((16, hidden))
router_w = rng.standard_normal((hidden, 1)) / math.sqrt(hidden)
gate = rng.standard_normal((1, hidden, intermediate)) / math.sqrt(hidden)
up = rng.standard_normal((1, hidden, intermediate)) / math.sqrt(hidden)
down = rng.standard_normal((1, intermediate, hidden)) / math.sqrt(intermediate)
layer = EducationalMoE(router_w, gate, up, down)
result = layer.forward(x, top_k=1)
dense = swiglu_expert(x, gate[0], up[0], down[0])
assert np.allclose(result.output, dense, atol=1e-12)
sg = rng.standard_normal((1, hidden, intermediate)) / math.sqrt(hidden)
su = rng.standard_normal((1, hidden, intermediate)) / math.sqrt(hidden)
sd = rng.standard_normal((1, intermediate, hidden)) / math.sqrt(intermediate)
with_shared = EducationalMoE(router_w, gate, up, down, shared_gate=sg, shared_up=su, shared_down=sd)
assert np.allclose(with_shared.forward(x, top_k=1).output, dense + swiglu_expert(x, sg[0], su[0], sd[0]))
print('экспертные ветви проверены')

4. Потери и управление нагрузкой

У z-loss сначала возводится в квадрат значение каждого токена; сравнение с квадратом среднего помогает заметить ошибочный порядок. Затем на одних оценках проследим смещения двух контроллеров — по знаку ошибки и пропорционального. Снижение CV означает более ровную нагрузку в этом опыте; веса экспертов не обучаются.

Python
def simulate_aux_free_balancing(*, mode: Literal["sign", "proportional"]) -> dict[str, Any]:
    """Reproduce deterministic 32-expert balancing traces.

    ``sign`` mirrors the main paper rule with a fixed-size correction. ``proportional`` is the
    smoother teaching controller used to visualize convergence; the paper also studies a
    proportional variant in an expert-parallel setting.
    """

    rng = np.random.default_rng(6)
    experts, top_k, token_count = 32, 4, 8192
    weights = rng.standard_normal((64, experts)) * 0.35
    constant = np.zeros(experts)
    constant[:4] = 2.5
    inputs = rng.standard_normal((token_count, 64))
    all_scores = stable_sigmoid(inputs @ weights + constant)

    def loads(bias: np.ndarray, scores: np.ndarray) -> np.ndarray:
        selected = _deterministic_top_k(scores + bias[None, :], top_k)
        return np.bincount(selected.ravel(), minlength=experts) / (scores.shape[0] * top_k)

    bias = np.zeros(experts)
    before = loads(bias, all_scores)
    rate = 0.001 if mode == "sign" else 0.15
    for _ in range(400):
        sample = all_scores[rng.integers(0, token_count, size=1024)]
        fractions = loads(bias, sample)
        bias = aux_free_bias_step(bias, fractions, rate=rate, mode=mode)
    after = loads(bias, all_scores)
    return {
        "mode": mode,
        "rate": rate,
        "before": before.tolist(),
        "after": after.tolist(),
        "bias": bias.tolist(),
        "uniform": 1.0 / experts,
        "cv_before": float(before.std() / before.mean()),
        "cv_after": float(after.std() / after.mean()),
    }


loss_ref = REFERENCE["losses"]
aux_logits = np.array(loss_ref["aux_logits"])
assert math.isclose(switch_auxiliary_loss(aux_logits), loss_ref["switch_aux"], rel_tol=1e-12)
assert math.isclose(router_z_loss(aux_logits), loss_ref["z_loss"], rel_tol=1e-12)
# Square before the token mean, not after it.
z = np.array([[0., 0.], [2., 2.]])
independent = (math.log(2)**2 + (2+math.log(2))**2)/2
assert math.isclose(router_z_loss(z), independent, rel_tol=1e-12)
assert not math.isclose(router_z_loss(z), (1+math.log(2))**2)
# The main paper rule is a fixed sign step.
bias = aux_free_bias_step(np.zeros(4), [8., 0., 0., 0.], rate=0.1, mode="sign")
assert np.allclose(bias, [-0.1, 0.1, 0.1, 0.1])
for mode in ("sign", "proportional"):
    got = simulate_aux_free_balancing(mode=mode)
    ref = REFERENCE["balance"][mode]
    assert np.allclose(got["before"], ref["before"], atol=1e-12)
    assert np.allclose(got["after"], ref["after"], atol=1e-12)
    assert math.isclose(got["cv_after"], ref["cv_after"], rel_tol=1e-12)
    print(mode, f"CV {got['cv_before']:.3f} -> {got['cv_after']:.3f}")
print('балансировка проверена')

5. Подсчёт параметров

Разделим весь банк и выбранный на токене путь, сохраняя смещения, нормы и правило учёта словарных матриц. Сверяйте компоненты до сравнения итогов с округлёнными паспортами. Поля Qwen читаются из встроенной конфигурации; одного отношения k/E недостаточно для активного счёта всей модели.

Python
@dataclass(frozen=True)
class MoEModelSpec:
    """Explicit parameter-counting contract for a dense/MoE Transformer.

    The class deliberately avoids pretending that one formula covers every MoE family. The
    caller states how many dense and MoE layers exist, whether the vocabulary matrices are tied,
    whether expert/router biases are present, and how many *full* vocabulary matrices are counted
    in the active-parameter convention. ``active_vocab_matrices=1`` is the compute-oriented
    convention used for gpt-oss: the dense output projection is counted, while an indexed input
    embedding lookup is not charged as a full matrix.
    """

    vocab_size: int
    hidden_size: int
    layers: int
    attention_params_per_layer: int
    routed_experts: int
    experts_per_token: int
    expert_intermediate: int
    shared_experts: int = 0
    dense_layers: int = 0
    dense_intermediate: int = 0
    vocab_matrices_total: int = 2
    active_vocab_matrices: int = 1
    router_bias: bool = False
    expert_bias: bool = False
    dense_bias: bool = False
    sink_params_per_layer: int = 0
    extra_total_parameters: int = 0
    extra_active_parameters: int = 0

    def __post_init__(self) -> None:
        for name in (
            "vocab_size", "hidden_size", "layers", "attention_params_per_layer",
            "routed_experts", "experts_per_token", "expert_intermediate",
            "vocab_matrices_total",
        ):
            _positive_int(name, getattr(self, name))
        for name in (
            "shared_experts", "dense_layers", "dense_intermediate",
            "active_vocab_matrices", "sink_params_per_layer",
            "extra_total_parameters", "extra_active_parameters",
        ):
            value = getattr(self, name)
            if isinstance(value, bool) or not isinstance(value, Integral) or int(value) < 0:
                raise ValueError(f"{name} must be a non-negative integer")
        if self.experts_per_token > self.routed_experts:
            raise ValueError("experts_per_token cannot exceed routed_experts")
        if self.dense_layers > self.layers:
            raise ValueError("dense_layers cannot exceed layers")
        if self.dense_layers and self.dense_intermediate == 0:
            raise ValueError("dense_intermediate is required when dense_layers > 0")
        if self.active_vocab_matrices > self.vocab_matrices_total:
            raise ValueError("active_vocab_matrices cannot exceed vocab_matrices_total")


@dataclass(frozen=True)
class ParameterBreakdown:
    """Total and active counts under one declared convention."""

    total_parameters: int
    active_parameters: int
    vocab_total: int
    vocab_active: int
    attention: int
    routed_and_shared_experts_total: int
    routed_and_shared_experts_active: int
    dense_ffn: int
    router: int
    norms: int
    sinks: int
    extra_total: int
    extra_active: int

    @property
    def total_to_active_ratio(self) -> float:
        return self.total_parameters / self.active_parameters


def gqa_attention_parameters(
    *,
    hidden_size: int,
    query_heads: int,
    kv_heads: int,
    head_width: int,
    include_bias: bool = False,
) -> int:
    """Count Q/K/V/O projections for one GQA layer.

    With ``include_bias=True`` the count matches the fused QKV bias and output bias in the
    public gpt-oss reference implementation.
    """

    hidden = _positive_int("hidden_size", hidden_size)
    q_heads = _positive_int("query_heads", query_heads)
    k_heads = _positive_int("kv_heads", kv_heads)
    width = _positive_int("head_width", head_width)
    if k_heads > q_heads:
        raise ValueError("kv_heads cannot exceed query_heads")
    q_dim = q_heads * width
    kv_dim = k_heads * width
    weights = hidden * q_dim + 2 * hidden * kv_dim + q_dim * hidden
    biases = (q_dim + 2 * kv_dim + hidden) if include_bias else 0
    return int(weights + biases)


def deepseek_v3_mla_attention_parameters() -> int:
    """Course reconstruction of one DeepSeek-V3 MLA layer from public configuration fields.

    Projection biases are absent in the published config. The final two terms are the Q/latent
    normalization scales. This is an explicit reconstruction, not a claim that the public 671B
    headline follows a unique counting convention.
    """

    hidden, q_lora, kv_lora, rope, heads, head_width = 7168, 1536, 512, 64, 128, 128
    return int(
        hidden * q_lora
        + q_lora * (heads * (head_width + rope))
        + hidden * (kv_lora + rope)
        + kv_lora * (heads * (head_width + head_width))
        + heads * head_width * hidden
        + q_lora
        + kv_lora
    )


def expert_parameters(*, hidden_size: int, intermediate_size: int, include_bias: bool) -> int:
    """Count one SwiGLU expert with separate gate/up/down matrices."""

    hidden = _positive_int("hidden_size", hidden_size)
    intermediate = _positive_int("intermediate_size", intermediate_size)
    weights = 3 * hidden * intermediate
    biases = (2 * intermediate + hidden) if include_bias else 0
    return int(weights + biases)


def count_moe_parameters(spec: MoEModelSpec) -> ParameterBreakdown:
    """Count a model exactly under ``spec`` and expose every convention."""

    hidden = spec.hidden_size
    moe_layers = spec.layers - spec.dense_layers
    vocab_unit = spec.vocab_size * hidden
    vocab_total = spec.vocab_matrices_total * vocab_unit
    vocab_active = spec.active_vocab_matrices * vocab_unit
    attention = spec.attention_params_per_layer * spec.layers

    expert = expert_parameters(
        hidden_size=hidden,
        intermediate_size=spec.expert_intermediate,
        include_bias=spec.expert_bias,
    )
    expert_total = expert * (spec.routed_experts + spec.shared_experts) * moe_layers
    expert_active = expert * (spec.experts_per_token + spec.shared_experts) * moe_layers

    dense_unit = 3 * hidden * spec.dense_intermediate
    if spec.dense_bias:
        dense_unit += 2 * spec.dense_intermediate + hidden
    dense_ffn = dense_unit * spec.dense_layers

    router_per_layer = hidden * spec.routed_experts
    if spec.router_bias:
        router_per_layer += spec.routed_experts
    router = router_per_layer * moe_layers
    norms = 2 * hidden * spec.layers + hidden
    sinks = spec.sink_params_per_layer * spec.layers

    total = (
        vocab_total + attention + expert_total + dense_ffn + router + norms + sinks
        + spec.extra_total_parameters
    )
    active = (
        vocab_active + attention + expert_active + dense_ffn + router + norms + sinks
        + spec.extra_active_parameters
    )
    return ParameterBreakdown(
        total_parameters=int(total),
        active_parameters=int(active),
        vocab_total=int(vocab_total),
        vocab_active=int(vocab_active),
        attention=int(attention),
        routed_and_shared_experts_total=int(expert_total),
        routed_and_shared_experts_active=int(expert_active),
        dense_ffn=int(dense_ffn),
        router=int(router),
        norms=int(norms),
        sinks=int(sinks),
        extra_total=int(spec.extra_total_parameters),
        extra_active=int(spec.extra_active_parameters),
    )


def bytes_to_gib(byte_count: int) -> float:
    """Convert a non-negative byte count to binary gibibytes."""

    if isinstance(byte_count, bool) or not isinstance(byte_count, Integral) or byte_count < 0:
        raise ValueError("byte_count must be a non-negative integer")
    return int(byte_count) / 2**30


def expected_active_expert_fraction(*, experts: int, top_k: int, token_count: int) -> float:
    """Expected touched fraction under independent uniform token-choice routing."""

    expert_count = _positive_int("experts", experts)
    k = _positive_int("top_k", top_k)
    tokens = _positive_int("token_count", token_count)
    if k > expert_count:
        raise ValueError("top_k cannot exceed experts")
    return float(1.0 - (1.0 - k / expert_count) ** tokens)


def simulate_active_expert_fraction(
    *, experts: int, top_k: int, token_count: int, trials: int, seed: int
) -> float:
    """Monte Carlo check of the uniform independent-routing law."""

    expert_count = _positive_int("experts", experts)
    k = _positive_int("top_k", top_k)
    tokens = _positive_int("token_count", token_count)
    repeats = _positive_int("trials", trials)
    if k > expert_count:
        raise ValueError("top_k cannot exceed experts")
    if isinstance(seed, bool) or not isinstance(seed, Integral):
        raise TypeError("seed must be an integer")
    rng = np.random.default_rng(int(seed))
    fractions = []
    for _ in range(repeats):
        selected = np.stack([rng.choice(expert_count, size=k, replace=False) for _ in range(tokens)])
        fractions.append(np.unique(selected).size / expert_count)
    return float(np.mean(fractions))


def expert_parallel_payload_bytes(
    *, token_count: int, hidden_size: int, top_k: int, bytes_per_value: int,
    include_return: bool = True,
) -> int:
    """Idealized activation payload; metadata, padding, and topology are excluded."""

    tokens = _positive_int("token_count", token_count)
    hidden = _positive_int("hidden_size", hidden_size)
    k = _positive_int("top_k", top_k)
    width = _positive_int("bytes_per_value", bytes_per_value)
    one_way = tokens * hidden * k * width
    return one_way * (2 if include_return else 1)


models = REFERENCE["models"]
gqa = gqa_attention_parameters(hidden_size=2880, query_heads=64, kv_heads=8, head_width=64, include_bias=True)
assert gqa == models["gqa_params_per_layer_with_bias"]
mla = deepseek_v3_mla_attention_parameters()
assert mla == models["mla_params_per_layer"]
specs = {
    "oss120": MoEModelSpec(201_088, 2880, 36, gqa, 128, 4, 2880,
        vocab_matrices_total=2, active_vocab_matrices=1, router_bias=True,
        expert_bias=True, sink_params_per_layer=64),
    "oss20": MoEModelSpec(201_088, 2880, 24, gqa, 32, 4, 2880,
        vocab_matrices_total=2, active_vocab_matrices=1, router_bias=True,
        expert_bias=True, sink_params_per_layer=64),
    "deepseek_v3_reconstruction": MoEModelSpec(129_280, 7168, 61, mla, 256, 8, 2048,
        shared_experts=1, dense_layers=3, dense_intermediate=18_432,
        vocab_matrices_total=2, active_vocab_matrices=1, router_bias=True),
}
for name, spec in specs.items():
    counted = count_moe_parameters(spec)
    assert asdict(counted) == models[name]
    print(name, f"total={counted.total_parameters:,} active={counted.active_parameters:,} ratio={counted.total_to_active_ratio:.3f}x")
qwen = REFERENCE["qwen35_config"]
for source, target in [("hidden_size", "hidden_size"), ("num_hidden_layers", "layers"),
    ("num_experts", "routed_experts"), ("num_experts_per_tok", "selected_experts"),
    ("moe_intermediate_size", "routed_intermediate"),
    ("shared_expert_intermediate_size", "shared_intermediate"),
    ("mtp_num_hidden_layers", "mtp_layers")]:
    assert QWEN_TEXT[source] == qwen[target]
assert qwen == {"hidden_size": 2048, "layers": 40, "routed_experts": 256,
    "selected_experts": 8, "shared_experts": 1, "routed_intermediate": 512,
    "shared_intermediate": 512, "mtp_layers": 1}
assert math.comb(32, 8) == 10_518_300
assert math.comb(256, 8) == REFERENCE["combinations"]["C256_8_unconstrained_upper_bound"]
print('параметры и конфигурация проверены')

6. Пакет и объём пересылки

Сначала вычислим ожидаемую долю банка, затронутую независимыми равномерными маршрутами. Затем посчитаем байты назначений при исходной и сжатой ширине. Логическая пересылка не равна измеренному сетевому трафику: размещение экспертов и протокол в эту модель не входят.

Python
for key, row in REFERENCE["active_expert_law"].items():
    e, k, n = [int(part[1:]) for part in key.split("_")]
    theory = expected_active_expert_fraction(experts=e, top_k=k, token_count=n)
    assert math.isclose(theory, row["theory"], rel_tol=1e-12)
    assert abs(theory - row["simulation"]) < 0.01
    print(key, f"theory={theory:.4f} simulation={row['simulation']:.4f}")
latent = REFERENCE["latent_moe"]
assert latent["kimi_k3_hidden"] / latent["kimi_k3_latent"] == latent["payload_reduction"] == 2.0
comm = REFERENCE["expert_parallel"]
payload = expert_parallel_payload_bytes(token_count=comm["tokens"], hidden_size=comm["hidden_size"],
    top_k=comm["top_k"], bytes_per_value=comm["bytes_per_value"], include_return=True)
assert payload == comm["round_trip_bytes"] == 448 * 2**20
print("Kimi K3 latent reduction", latent["payload_reduction"], "x | EP payload", payload / 2**20, "MiB")
print('объём пересылки проверен')

7. Проверки неверных входов

Ошибочные формы, единицы и вырожденные входы должны дать понятную ошибку.

Python
def expect_failure(exc, fn, *args, **kwargs):
    try:
        fn(*args, **kwargs)
    except exc:
        return
    raise AssertionError(f"expected {exc.__name__} from {fn.__name__}")

expect_failure(ValueError, route_logits, [[1., 2.]], top_k=3)
expect_failure(ValueError, route_logits, [[1., np.nan]], top_k=1)
expect_failure(ValueError, expert_capacity, token_count=8, top_k=2, expert_count=4, capacity_factor=0.)
expect_failure(ValueError, load_statistics, [0., 0.])
expect_failure(ValueError, aux_free_bias_step, [0., 0.], [1., 1.], rate=.1, mode="unknown")
expect_failure(ValueError, bytes_to_gib, -1)
expect_failure(ValueError, expert_parallel_payload_bytes, token_count=4, hidden_size=8, top_k=2, bytes_per_value=0)
expect_failure(ValueError, MoEModelSpec, 100, 64, 8, 1024, 4, 2, 128, dense_layers=2, dense_intermediate=0)
print('отрицательные тесты пройдены')

PyTorch bridge · B09 — MoE top-k: выбор и веса смеси

Эта ячейка встраивает канонический NumPy→PyTorch-мост и выполняет проверку эквивалентности. Код самодостаточен и не импортирует файлы репозитория.

Python
import importlib.util
import subprocess
import sys

missing = []
if importlib.util.find_spec("numpy") is None:
    missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
    missing.append("torch>=2.5")
if missing:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])

import numpy as np
import torch
import torch.nn.functional as F
import math

def numpy_topk_router_explicit(
    logits: np.ndarray,
    top_k: int,
    *,
    selection_bias: np.ndarray | None = None,
) -> tuple[np.ndarray, np.ndarray]:
    """Minimal sigmoid router: bias changes selection, not clean mixture weights."""
    logits = np.asarray(logits, dtype=np.float64)
    if logits.ndim != 2 or not logits.size or not np.isfinite(logits).all():
        raise ValueError("expected non-empty finite logits[T,E]")
    if isinstance(top_k, (bool, np.bool_)) or not isinstance(top_k, (int, np.integer)) or not 1 <= top_k <= logits.shape[1]:
        raise ValueError("top_k must be an integer in [1,E]")
    log_scores = -np.logaddexp(0.0, -logits)
    clean = np.exp(log_scores)
    # Without bias, monotonic sigmoid permits ranking logits before saturation.
    selection = logits if selection_bias is None else clean + selection_bias
    if selection.shape != logits.shape or not np.isfinite(selection).all():
        raise ValueError("selection_bias must broadcast to [T,E] and be finite")
    indices = np.argsort(-selection, axis=-1, kind="stable")[:, :top_k]
    selected_log = np.take_along_axis(log_scores, indices, axis=-1)
    weights = np.exp(selected_log - selected_log.max(axis=-1, keepdims=True))
    weights = weights / weights.sum(axis=-1, keepdims=True)
    return indices, weights

def torch_topk_router_explicit(
    logits: torch.Tensor,
    top_k: int,
    *,
    selection_bias: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Minimal tensor counterpart using sigmoid, topk, and gather."""
    if logits.ndim != 2 or not logits.numel() or not logits.is_floating_point() or not torch.isfinite(logits).all():
        raise ValueError("expected non-empty finite floating logits[T,E]")
    if isinstance(top_k, bool) or not isinstance(top_k, int) or not 1 <= top_k <= logits.shape[1]:
        raise ValueError("top_k must be an integer in [1,E]")
    z = logits.float() if logits.dtype in (torch.float16, torch.bfloat16) else logits
    log_scores = -torch.logaddexp(torch.zeros_like(z), -z)
    selection = z if selection_bias is None else log_scores.exp() + selection_bias
    if selection.shape != logits.shape or not torch.isfinite(selection).all():
        raise ValueError("selection_bias must broadcast to [T,E] and be finite")
    _, indices = torch.topk(selection, top_k, dim=-1)
    selected_log = log_scores.gather(-1, indices)
    weights = selected_log.softmax(dim=-1)
    return indices, weights

logits_np = np.array([[2.1, -0.4, 0.8, 1.3], [0.2, 1.9, -1.1, 0.7]])
bias_np = np.array([0.0, 0.08, -0.03, 0.02])
i_np, w_np = numpy_topk_router_explicit(logits_np, 2, selection_bias=bias_np)
i_t, w_t = torch_topk_router_explicit(torch.tensor(logits_np), 2, selection_bias=torch.tensor(bias_np))
np.testing.assert_array_equal(i_np, i_t.numpy())
np.testing.assert_allclose(w_np, w_t.numpy())
extreme = np.array([[-1002., -1000., -1001., -1003.]])
with np.errstate(over="raise", invalid="raise", divide="raise"):
    ei, ew = numpy_topk_router_explicit(extreme, 2)
ti, tw = torch_topk_router_explicit(torch.tensor(extreme), 2)
np.testing.assert_array_equal(ei, [[1, 2]])
np.testing.assert_array_equal(ei, ti.numpy())
np.testing.assert_allclose(ew, tw.numpy(), rtol=1e-12, atol=1e-12)
z = torch.tensor(logits_np, requires_grad=True)
assert torch.autograd.gradcheck(lambda x: torch_topk_router_explicit(x, 2)[1], (z,))
one = torch_topk_router_explicit(z, 1)[1]
np.testing.assert_allclose(torch.autograd.grad(one.sum(), z)[0].numpy(), 0., atol=1e-12)
print("B09 routing, extreme logits, and selected-gate gradients: PASS")

PyTorch bridge · B10 — MoE dispatch/combine

Эта ячейка встраивает канонический NumPy→PyTorch-мост и выполняет проверку эквивалентности. Код самодостаточен и не импортирует файлы репозитория.

Входы имеют формы hidden [T,D], индексы и веса смеси [T,K], матрицы экспертов [E,D,F] и [E,F,D]. Индексы — целые числа от 0 до E−1; одинаковые номера в разных назначениях допустимы, их вклады складываются. Вещественные массивы конечны и используют совместимый плавающий тип; в PyTorch все тензоры находятся на одном устройстве. Минимальный мост проверяет индексы, затем раскрывает отправку и суммирование; вместимость здесь не ограничена.

Python
import importlib.util
import subprocess
import sys

missing = []
if importlib.util.find_spec("numpy") is None:
    missing.append("numpy>=1.26")
if importlib.util.find_spec("torch") is None:
    missing.append("torch>=2.5")
if missing:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", *missing])

import numpy as np
import torch
import torch.nn.functional as F
import math

def numpy_expert_dispatch_explicit(
    hidden: np.ndarray,
    expert_indices: np.ndarray,
    gates: np.ndarray,
    w_in: np.ndarray,
    w_out: np.ndarray,
) -> np.ndarray:
    """Minimal token/slot loop that makes dispatch and weighted combine visible."""
    if expert_indices.ndim != 2 or expert_indices.shape != gates.shape or expert_indices.shape[0] != hidden.shape[0]:
        raise ValueError("indices and gates must have matching shape [T,K]")
    if not np.issubdtype(expert_indices.dtype, np.integer):
        raise ValueError("expert indices must be integers")
    if np.any(expert_indices < 0) or np.any(expert_indices >= w_in.shape[0]):
        raise ValueError("expert index out of range")
    output = np.zeros_like(hidden)
    for token in range(hidden.shape[0]):
        for slot in range(expert_indices.shape[1]):
            expert = int(expert_indices[token, slot])
            mid = np.tanh(hidden[token] @ w_in[expert])
            output[token] += gates[token, slot] * (mid @ w_out[expert])
    return output

def torch_expert_dispatch_explicit(
    hidden: torch.Tensor,
    expert_indices: torch.Tensor,
    gates: torch.Tensor,
    w_in: torch.Tensor,
    w_out: torch.Tensor,
) -> torch.Tensor:
    """Group assignments by expert and combine them with index_add_."""
    if expert_indices.ndim != 2 or expert_indices.shape != gates.shape or expert_indices.shape[0] != hidden.shape[0]:
        raise ValueError("indices and gates must have matching shape [T,K]")
    if expert_indices.dtype not in (torch.int8, torch.int16, torch.int32, torch.int64, torch.uint8):
        raise ValueError("expert indices must be integers")
    if torch.any(expert_indices < 0) or torch.any(expert_indices >= w_in.shape[0]):
        raise ValueError("expert index out of range")
    output = torch.zeros_like(hidden)
    for expert in range(w_in.shape[0]):
        token_ids, slots = torch.where(expert_indices == expert)
        if token_ids.numel() == 0:
            continue
        mid = torch.tanh(hidden[token_ids] @ w_in[expert])
        expert_out = (mid @ w_out[expert]) * gates[token_ids, slots, None]
        output.index_add_(0, token_ids, expert_out)
    return output

rng = np.random.default_rng(10)
x_np = rng.standard_normal((6, 4))
idx_np = np.array([[0,1],[1,2],[2,0],[0,2],[1,0],[2,1]])
gates_np = np.full((6,2), 0.5)
w1_np = rng.standard_normal((3,4,5)); w2_np = rng.standard_normal((3,5,4))
np_out = numpy_expert_dispatch_explicit(x_np, idx_np, gates_np, w1_np, w2_np)
t_out = torch_expert_dispatch_explicit(torch.tensor(x_np), torch.tensor(idx_np), torch.tensor(gates_np), torch.tensor(w1_np), torch.tensor(w2_np))
np.testing.assert_allclose(np_out, t_out.numpy(), rtol=1e-10, atol=1e-10)
args = [torch.tensor(x_np, requires_grad=True), torch.tensor(idx_np),
        torch.tensor(gates_np, requires_grad=True), torch.tensor(w1_np, requires_grad=True),
        torch.tensor(w2_np, requires_grad=True)]
assert torch.autograd.gradcheck(
    lambda x, g, a, b: torch_expert_dispatch_explicit(x, args[1], g, a, b),
    (args[0], args[2], args[3], args[4]))
for invalid in (-1, w1_np.shape[0], .5, np.nan, True):
    bad = np.full(idx_np.shape, invalid)
    for dispatch, values in (
        (numpy_expert_dispatch_explicit, (x_np,bad,gates_np,w1_np,w2_np)),
        (torch_expert_dispatch_explicit, tuple(torch.tensor(a) for a in (x_np,bad,gates_np,w1_np,w2_np))),
    ):
        try: dispatch(*values)
        except ValueError: pass
        else: raise AssertionError("invalid expert index accepted")
duplicate = np.zeros_like(idx_np)
expected = idx_np.shape[1] * (np.tanh(x_np @ w1_np[0]) @ w2_np[0])
actual = numpy_expert_dispatch_explicit(x_np,duplicate,np.ones_like(gates_np),w1_np,w2_np)
np.testing.assert_allclose(actual,expected)
actual_t = torch_expert_dispatch_explicit(torch.tensor(x_np),torch.tensor(duplicate),
    torch.ones_like(torch.tensor(gates_np)),torch.tensor(w1_np),torch.tensor(w2_np))
np.testing.assert_allclose(actual_t.numpy(),expected)
print("B10 values, gradients, repeated assignments, and invalid indices: PASS")

Упражнения

  1. При переполнении принимайте назначения с наибольшими весами смеси. Сравните с FIFO.

  2. Перенаправьте отброшенное назначение следующему доступному эксперту; объявите правило новых весов.

  3. Сравните долю затронутых экспертов с независимой равномерной моделью на коррелированных маршрутах.

  4. Уменьшите размерность LatentMoE с 3584 до 1792. Посчитайте данные пересылки и веса проекторов.

  5. Для top-1 после перенормировки вес равен 1. Через какой путь роутер получает градиент? Проверьте B09.

  6. Перенесите слой на PyTorch и сравните результаты и градиенты до измерения скорости.

Симуляция фиксирует оценки роутера и обновляет только смещения. Она не обучает языковую модель и не измеряет скорость кластера. Общие эксперты остаются активными при потере всех выбираемых назначений. B10 использует простые tanh-эксперты, чтобы выделить операцию сборки результатов; основной слой выше использует SwiGLU.

Демо проектных упражнений модуля 7 — Балансировка, совместные назначения и вмешательства

Проектная демонстрация

Скачать .ipynb module_07_project_demo.ipynb · 11,7 КБ

Показать текст ноутбука

Демо проектных упражнений модуля 7 — Балансировка, совместные назначения и вмешательства

Демо-реализация, не готовое решение полномасштабного проекта. Синтетические кластеры позволяют проследить маршруты. Роутер и эксперты здесь не обучаются, а кластеры не являются доказанными семантическими специализациями. Все входы малы, детерминированы и работают без сети на CPU. Цель — проверить интерфейсы, метрики, инварианты и структуру отчёта до расходования GPU/API-бюджета.

Python
from __future__ import annotations
import importlib.util, subprocess, sys
for package in ("numpy", "matplotlib"):
    if importlib.util.find_spec(package) is None:
        subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])


import math
import random
import time
from collections import Counter, defaultdict

import matplotlib.pyplot as plt
import numpy as np

SEED = 20260801
random.seed(SEED)
np.random.seed(SEED)
print(f"воспроизводимый тестовый запуск; seed={SEED}")
DEMO_LANGUAGE = 'ru'

plt.rcParams.update({"font.family":"DejaVu Sans", "font.size":12, "axes.titlesize":15, "axes.labelsize":13, "xtick.labelsize":11, "ytick.labelsize":11, "figure.dpi":150})

Демо 1. Балансировка при неизменных оценках

Создадим 900 векторов из трёх синтетических кластеров и шесть экспертов с выбором top-2. Сам роутер не обучается: сто раз меняются только смещения выбора. На каждом шаге сравниваем нагрузки с их средним и делаем шаг по знаку отклонения. Это позволяет увидеть работу контроллера отдельно от обучения весов.

Слева проследите коэффициент вариации нагрузок. Справа каждая колонка делится на 600 — число назначений для 300 токенов кластера при top-2. Поэтому колонка суммируется к единице и показывает долю назначений, а не долю токенов, для которых эксперт выбран хотя бы раз.

Python
ru = DEMO_LANGUAGE == "ru"
rng = np.random.default_rng(SEED)
cluster = np.repeat(np.arange(3), 300)
features = rng.normal(size=(900, 8)) + np.eye(3, 8)[cluster] * 2
router_w = rng.normal(size=(8, 6))
logits = features @ router_w
log_scores = -np.logaddexp(0., -logits)
scores = np.exp(log_scores)

def route(bias):
    indices = np.argsort(-(scores + bias), axis=1, kind="stable")[:, :2]
    selected = np.take_along_axis(log_scores, indices, axis=1)
    gates = np.exp(selected - selected.max(axis=1, keepdims=True))
    gates /= gates.sum(axis=1, keepdims=True)
    return indices, gates, np.bincount(indices.ravel(), minlength=6)

bias = np.zeros(6)
history = []
for step in range(101):
    top2, gates, counts = route(bias)
    history.append(counts.std() / counts.mean())
    if step < 100:
        bias += .01 * np.sign(counts.mean() - counts)
assert history[-1] < history[0]
assert counts.sum() == 1800
print("CV:", round(history[0], 4), "→", round(history[-1], 4))
fig, axes = plt.subplots(1, 2, figsize=(11, 4.8), layout="constrained")
axes[0].plot(history, color="#5E81AC")
axes[0].set(xlabel="Обновления смещений" if ru else "Bias updates", ylabel="CV",
            title="Фиксированные оценки" if ru else "Fixed router scores")
heat = np.zeros((6, 3), dtype=int)
for c, selected in zip(cluster, top2):
    heat[selected, c] += 1
im = axes[1].imshow(heat / 600, cmap="Blues", vmin=0, vmax=1, aspect="auto")
axes[1].set(xlabel="Синтетический кластер" if ru else "Synthetic cluster",
            ylabel="Эксперт" if ru else "Expert", xticks=[0,1,2],
            title="Доля назначений в кластере" if ru else "Assignment share within cluster")
fig.colorbar(im, ax=axes[1])
assert np.allclose((heat / 600).sum(axis=0), 1.)
plt.show()

Снижение CV показывает более равномерную нагрузку в этой популяции оценок. Различия между колонками могут сохраниться: общий баланс не требует одинаковых маршрутов каждого кластера. Ни качество задачи, ни семантические навыки здесь не измеряются.

Демо 2. Частота совместного выбора и эффект вмешательства

Для каждой пары top-2 запишем два направленных счётчика. После деления строки на частоту выбора эксперта получим распределение его партнёра. Затем зададим фиксированные случайные FFN и по очереди обнулим вклад каждого эксперта. Маршруты и остальные веса смеси оставим прежними: перенормировка или повторный выбор были бы другими вмешательствами.

Python
# Count co-activation separately from the effect of changing expert outputs.
co = np.zeros((6, 6), dtype=int)
for a, b in top2:
    co[a, b] += 1
    co[b, a] += 1
assert np.all(co == co.T) and np.all(np.diag(co) == 0)
assert np.array_equal(co.sum(axis=1), counts)  # exactly one partner at top-2
conditional = co / counts[:, None]
assert np.allclose(conditional.sum(axis=1), 1.)
w1 = rng.normal(scale=.3, size=(6, 8, 12))
w2 = rng.normal(scale=.3, size=(6, 12, 8))
expert_outputs = np.stack([np.tanh(features @ a) @ b for a,b in zip(w1,w2)], axis=1)
selected_outputs = expert_outputs[np.arange(len(features))[:, None], top2]
original = (gates[..., None] * selected_outputs).sum(axis=1)
effects = []
for expert in range(6):
    # Zero an expert contribution while keeping routes and all other gates fixed.
    kept = gates * (top2 != expert)
    intervened = (kept[..., None] * selected_outputs).sum(axis=1)
    effects.append(np.mean((intervened - original)**2))
assert np.all(np.isfinite(effects)) and min(effects) >= 0
fig, axes = plt.subplots(1,2,figsize=(11,4.8),layout="constrained")
im=axes[0].imshow(conditional,cmap="Blues",vmin=0,vmax=1)
axes[0].set(xlabel="Партнёр" if ru else "Partner expert",ylabel="Выбранный эксперт" if ru else "Selected expert",
    title="P(партнёр | эксперт)" if ru else "P(partner | expert)")
fig.colorbar(im,ax=axes[0])
axes[1].bar(range(6),effects,color="#D08770")
axes[1].set(xlabel="Обнулённый эксперт" if ru else "Zeroed expert",
    ylabel="MSE выхода" if ru else "Output MSE",
    title="При неизменных маршрутах" if ru else "With routes held fixed")
plt.show()
print("Это изменение вектора, не падение качества задачи." if ru else "This measures vector change, not task-quality loss.")

Матрица совместного выбора до нормировки симметрична, но условные вероятности не обязаны быть симметричными: знаменатели строк различаются. Столбцы второго графика измеряют средний квадрат изменения выходного вектора. Чтобы говорить о потере способности, понадобится обученная модель и метрика соответствующей задачи.

Переход к полному проекту

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

Минимальный отчёт

  1. Версии данных, модели, кода и seed.

  2. Основная метрика, а также качество, стоимость, задержка и память там, где это применимо.

  3. Базовая линия, одинаковый бюджет и хотя бы один отрицательный контроль.

  4. Неудачные прогоны и границы обоснованных выводов.