Skip to content
AILinkDeepTech
Go back
Deep Learning Medium

Mixture of Experts (MoE) Implementation in PyTorch

Abstract

A PyTorch implementation of the Mixture of Experts (MoE) layer with a top-k gating network, multiple expert MLPs, and weighted expert outputs for sparse expert routing.

Mixture of Experts (MoE) Implementation in PyTorch

This implementation builds a Mixture of Experts (MoE) layer from scratch. It includes a top-k gating network that scores and selects experts per token, multiple expert MLPs that process routed inputs, and a weighted combination of expert outputs.

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

class ExpertModule(nn.Module):
    """
    expert network - simplified MLP version
    """
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, output_size)
        self.activation = nn.ReLU()

    def forward(self, x):
        x = self.activation(self.fc1(x))
        x = self.fc2(x)
        return x
    
class MoEGating(nn.Module):
    """
    Gating network that determines which experts to use for each input
    """
    def __init__(self, input_size, num_experts, top_k=2):
        super().__init__()
        self.input_size = input_size
        self.num_experts = num_experts
        self.top_k = top_k
        # Gating weights for expert selection
        self.weight = nn.Parameter(torch.empty(num_experts, input_size))
        self.reset_parameters()

    def reset_parameters(self):
        nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))

    def forward(self, x):
        x_flat = x.view(-1, self.input_size)

        # Compute expert scores using linear projection
        scores = F.linear(x_flat, self.weight)
        scores = torch.sigmoid(scores)  

        # top-k experts
        top_k_scores, top_k_indices = torch.topk(scores, k=self.top_k, dim=-1)

        # Normalize 
        top_k_scores = top_k_scores / (top_k_scores.sum(dim=-1, keepdim=True) + 1e-10)

        return top_k_indices, top_k_scores
    
class MixtureOfExperts(nn.Module):
    """
    Complete Mixture of Experts model
    """
    def __init__(self, input_size, hidden_size, output_size, num_experts, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # expert networks
        self.experts = nn.ModuleList([
            ExpertModule(input_size, hidden_size, output_size)
            for _ in range(num_experts)
        ])

        # gating network
        self.gate = MoEGating(input_size, num_experts, top_k)

    def forward(self, x):
        # Get expert assignments and weights from gating network
        expert_indices, expert_weights = self.gate(x)

        batch_size, seq_len, _ = x.shape
        x_flat = x.view(-1, x.shape[-1])
        
        final_output = torch.zeros(
            (batch_size * seq_len, self.experts[0].fc2.out_features),
            device=x.device,
            dtype=x.dtype
        )

        for k in range(self.top_k):
            # Get expert indices and weights for current k
            current_indices = expert_indices[:, k]
            current_weights = expert_weights[:, k]

            for i in range(self.num_experts):
                mask = (current_indices == i)
                if mask.any():
                    expert_input = x_flat[mask]
                    expert_output = self.experts[i](expert_input)
                    # Add weighted output to final result
                    final_output[mask] += expert_output * current_weights[mask].unsqueeze(-1)

        output = final_output.view(batch_size, seq_len, -1)
        return output
    
def test_moe():
    input_size = 64
    hidden_size = 128
    output_size = 32
    num_experts = 4
    batch_size = 16
    seq_len = 8

    model = MixtureOfExperts(
        input_size=input_size,
        hidden_size=hidden_size,
        output_size=output_size,
        num_experts=num_experts
    )
    
    x = torch.randn(batch_size, seq_len, input_size)

    output = model(x)
    
    print("\nRunning basic tests...")
    print(f"Input shape: {x.shape}")
    print(f"Output shape: {output.shape}")
    assert output.shape == (batch_size, seq_len, output_size), "Output shape mismatch"

    print("\nTesting gating network...")
    expert_indices, expert_weights = model.gate(x)
    print(f"Expert indices shape: {expert_indices.shape}")
    print(f"Expert weights shape: {expert_weights.shape}")
    assert expert_indices.shape == (batch_size * seq_len, model.top_k), "Expert indices shape mismatch"
    assert expert_weights.shape == (batch_size * seq_len, model.top_k), "Expert weights shape mismatch"

    print("\nTesting weight normalization...")
    weight_sums = expert_weights.sum(dim=-1)
    assert torch.allclose(weight_sums, torch.ones_like(weight_sums), atol=1e-6), "Weights don't sum to 1"
    print("Weight normalization test passed")

    print("\nAll tests passed!")

if __name__ == "__main__":
    test_moe()


Cite this Explanation

@article{ailinkdeeptech2025moealgo,
  title={Mixture of Experts (MoE) Implementation in PyTorch},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/moe_algo}
}

Related Explanations