← All Problems

14. Mixture-of-Experts Top-2 Routing Layer

General Hard OpenAI-Style PyTorch Rounds
Grounding: General pattern common across ML-research-lab technical interviews, not tied to one specific reported example. MoE routing is well-established public architecture knowledge (Shazeer et al. 2017; Switch Transformer, 2021) and plausibly relevant to Mistral AI given their public focus on efficient open-weight MoE models — but that connection is this problem author's inference from Mistral's known product line, not a sourced or confirmed interview report; no source in this research directly confirms an MoE-routing coding question at any of these labs.

Problem

Sparse mixture-of-experts (MoE) layers scale model capacity without scaling per-token compute: instead of running every token through one big feed-forward block, a small router picks a handful of "experts" per token from a larger pool, and only those experts run. Top-2 gating (Shazeer et al. 2017's Sparsely-Gated MoE; used in Switch Transformer and Mixtral-style models) routes each token to its two highest-scoring experts and combines their outputs weighted by a softmax computed over just those two logits, not the full expert pool.

Given x (shape (batch, d_model)), gate_logits (shape (batch, num_experts)), and expert_weights (shape (num_experts, d_model, d_model), each a plain linear map with no bias), select each token's top-2 experts, combine their outputs by a locally-renormalized softmax, and return the combined output plus which experts were picked and their weights.

Source: src/14_moe_top2_routing.py

def moe_top2_forward(x, gate_logits, expert_weights) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]

>>> import torch
>>> torch.manual_seed(0)
>>> x = torch.tensor([[1.0, 0.0, 0.0]])
>>> gate_logits = torch.tensor([[1.0, 5.0, 0.5, 2.0]])
>>> expert_weights = torch.randn(4, 3, 3)
>>> out, idx, w = moe_top2_forward(x, gate_logits, expert_weights)
>>> idx
tensor([[1, 3]])

Step-by-Step Approach

  1. Select each token's top-2 experts with torch.topk(gate_logits, k=2, dim=-1), which returns both the top-2 logit values and the expert indices they belong to, already ranked highest first.
  2. Softmax only the two selected logits with torch.softmax(topk_logits, dim=-1) — this is what makes it "top-2 gating" instead of a full soft mixture over every expert, and is the detail most easily gotten wrong (softmaxing the full gate_logits row first and then indexing into it gives the wrong weights, since the unselected experts would still pull normalization mass).
  3. Gather each token's two chosen expert weight matrices in a single indexing operation: expert_weights[topk_idx] broadcasts the (batch, 2) index tensor into a (batch, 2, d_model, d_model) gathered weight tensor.
  4. Apply each chosen expert's linear map to its token's input with torch.einsum("bnji,bi->bnj", chosen_W, x), avoiding ever materializing a per-token dense pass through all num_experts experts.
  5. Combine the two expert outputs by their gating weights: broadcast-multiply topk_weights.unsqueeze(-1) against the expert outputs and sum over the "which of the 2 experts" dimension.

The key insight is that "top-2 gating" specifically means the softmax normalization happens after selection, over only the selected experts' logits — this is what keeps a token's total routing weight summing to exactly 1 across its two chosen experts regardless of how many total experts exist, and is the detail that distinguishes true top-k MoE gating from a "soft mixture with hard top-k masking" that superficially looks similar but computes different combination weights.

Reference solution

import torch


def moe_top2_forward(x, gate_logits, expert_weights):
    # pick each token's top-2 experts by router score; topk already returns
    # them ranked highest-first, which we rely on for tests/inspection
    topk_logits, topk_idx = torch.topk(gate_logits, k=2, dim=-1)  # (batch, 2)
    # softmax over ONLY the 2 selected logits -- not the full expert pool --
    # is what makes this "top-2 gating" rather than a full soft mixture
    topk_weights = torch.softmax(topk_logits, dim=-1)  # (batch, 2)

    # gather each token's two chosen expert weight matrices in one indexing op
    chosen_W = expert_weights[topk_idx]  # (batch, 2, d_model, d_model)
    # apply y = x @ W^T per selected expert without materializing a (batch,
    # num_experts, ...) tensor for experts that weren't even selected
    expert_out = torch.einsum("bnji,bi->bnj", chosen_W, x)  # (batch, 2, d_model)
    # combine the two expert outputs by their gating weights
    output = (topk_weights.unsqueeze(-1) * expert_out).sum(dim=1)  # (batch, d_model)

    return output, topk_idx, topk_weights

Key Functions & Tricks

  • torch.topk(gate_logits, k=2, dim=-1) — selects both which experts and their raw scores in one call, pre-ranked.
  • torch.softmax(topk_logits, dim=-1) applied post-selection — the defining detail of top-k gating versus a full soft mixture.
  • expert_weights[topk_idx] — advanced/fancy indexing that turns a (batch, 2) index tensor into a per-token gathered batch of weight matrices without a Python loop over tokens.
  • torch.einsum("bnji,bi->bnj", chosen_W, x) — applies a per-token, per-expert-slot linear map in one call; the index letters make explicit which dims are batched (b), which are the "slot" dim (n), and which are contracted (i).
  • Tensor.unsqueeze(-1) then broadcast-multiply and .sum(dim=1) — the standard weighted-combination pattern once per-slot outputs and per-slot weights are aligned.

How to Recognize This Pattern

Any "select a subset, weight it, combine" architecture (MoE gating, multi-head attention output projection with head dropout, ensemble-of-experts voting) shares the select-then-locally-renormalize-then-combine shape. The signal is a router or gate producing scores over more candidates than are actually used downstream. Common variations include top-1 (hard) routing instead of top-2, and adding a load-balancing auxiliary loss to discourage all tokens routing to the same expert (not required here, but a natural follow-up question). The most common pitfall is normalizing the gate weights before selection instead of after, which silently changes the combination weights whenever num_experts > 2.