AI Grimoire
Sheet
statuscommon
difficultyadvanced
timeO(E)
described2024
revisedtoday

Loss-Free Load Balancing

The auxiliary loss balances experts by making the model worse at predicting text. Replace it with a bias that is nudged up or down by a control loop and the objective is left alone.

[sparse][training]Commonly used

Standing

Commonly usedEstablished and frequently the right choice, but competing with live alternatives rather than having settled the question.

Introduced for DeepSeek-V3 and adopted quickly. It removes a term that was known to be in tension with the language-modelling objective, which is a rare kind of improvement.

judged as of 2026-09 · what the labels mean

Theory

The auxiliary balancing loss works by adding a term to the objective that is minimised when routing is uniform. It therefore competes with the language-modelling term, and the coefficient α\alpha sets the exchange rate between “balanced” and “correct”.

There is no good value. Small α\alpha lets the router collapse; large α\alpha pushes it towards uniform routing, which is routing that carries no information. The whole apparatus is a workaround for a constraint that is not really part of the learning problem — balance is an engineering requirement about buffers and devices, and it has been smuggled into the loss.

A bias instead of a gradient

Give each expert a scalar bib_i, added to its score for the purposes of selection only, and update it outside the optimiser.

topk(si+bi)selected, then combined withgi=sijtopksj\mathrm{top}_k\bigl(s_i + b_i\bigr) \qquad\text{selected, then combined with}\qquad g_i = \frac{s_i}{\sum_{j \in \mathrm{top}_k} s_j}
eq. 1 — the bias picks; the raw score weights
bibi+γsign(cˉci)b_i \leftarrow b_i + \gamma \cdot \mathrm{sign}\bigl(\bar{c} - c_i\bigr)
eq. 2 — a proportional controller, one step per batch

where cic_i is the number of tokens expert ii received and cˉ\bar{c} the mean. Overloaded experts get their bias lowered, starved ones get it raised, and the system settles. It is a control loop, not an optimisation: bb receives no gradient and appears nowhere in the backward pass.

Why sign rather than magnitude

The update uses sign\mathrm{sign}, not the imbalance itself. That makes every step the same size, which sounds crude and is deliberate: an expert that is briefly overloaded because of an unusual batch gets the same small nudge as one that is persistently overloaded, so the controller responds to sustained imbalance and ignores noise. A proportional update on the raw counts oscillates.

γ=0.001\gamma = 0.001 against per-expert scores of order 1 means the bias takes hundreds of steps to move appreciably — slow relative to the router, which is the correct relationship between a controller and the thing it controls.

Implementation

python · torch
import torch
from torch import Tensor, nn


class LossFreeRouter(nn.Module):
    def __init__(self, dim: int, n_experts: int, k: int, gamma: float = 1e-3):
        super().__init__()
        self.gate = nn.Linear(dim, n_experts, bias=False)
        # A buffer, not a parameter: it must not reach the optimiser.
        self.register_buffer("bias", torch.zeros(n_experts))
        self.k, self.gamma = k, gamma

    def forward(self, x: Tensor) -> tuple[Tensor, Tensor]:    # [T, D]
        scores = self.gate(x).sigmoid()                       # [T, E]

        idx = (scores + self.bias).topk(self.k, dim=-1).indices
        weight = scores.gather(-1, idx)
        weight = weight / weight.sum(-1, keepdim=True)        # unbiased weights

        if self.training:
            self._rebalance(idx, scores.size(-1))
        return idx, weight

    @torch.no_grad()
    def _rebalance(self, idx: Tensor, n_experts: int) -> None:
        load = torch.bincount(idx.flatten(), minlength=n_experts).float()
        # sign, not magnitude: respond to sustained imbalance, ignore noise.
        self.bias += self.gamma * torch.sign(load.mean() - load)

Two things about that buffer. It must be excluded from weight decay, which would otherwise pull it towards zero and fight the controller. And under data parallelism the load counts must be all-reduced before the update, or each rank maintains a different bias and the ranks diverge — a bug that shows up as balanced routing on each device and unbalanced routing globally.

The remaining caveat

The controller balances over whatever window it sees. DeepSeek-V3 pairs it with a small sequence-level auxiliary loss anyway, at a weight low enough not to matter for the objective, because a batch can be globally balanced while every individual sequence routes to one expert — and a long generation that hammers a single expert is a latency problem even when the training statistics look fine.

Aux-loss weight
0
Bias update rate γ
0.001
Gradient contribution
none
DeepSeek-V3

Related

References

[1]Wang et al. — Auxiliary-Loss-Free Load Balancing Strategy for Mixture-of-Experts (2024)arXiv:2408.15664
[2]DeepSeek-AI — DeepSeek-V3 Technical Report (2024)arXiv:2412.19437
[3]Fedus et al. — Switch Transformers (2021)arXiv:2101.03961