Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

moe_code.py

41_mixture_of_experts/moe_code.py  ·  354 lines  ·  view on GitHub

"""
Mixture of Experts: Complete Implementation
Simple, interview-writable code
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import List, Tuple

# ==================== BASIC MOE LAYER ====================

class Expert(nn.Module):
    """
    Single Expert Network
    
    Standard feed-forward network
    """
    def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)
        self.activation = nn.ReLU()
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Standard FFN: FFN(x) = ReLU(xW1 + b1)W2 + b2
        
        Args:
            x: Input, shape (batch, seq_len, d_model)
        Returns:
            Output, shape (batch, seq_len, d_model)
        """
        x = self.linear1(x)
        x = self.activation(x)
        x = self.dropout(x)
        x = self.linear2(x)
        return x


class Router(nn.Module):
    """
    Router/Gating Network
    
    Decides which experts to activate
    """
    def __init__(self, d_model: int, num_experts: int):
        super().__init__()
        self.gate = nn.Linear(d_model, num_experts)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Compute expert scores
        
        Args:
            x: Input, shape (batch, seq_len, d_model)
        Returns:
            Expert scores, shape (batch, seq_len, num_experts)
        """
        return self.gate(x)  # Logits, not probabilities yet


class MixtureOfExperts(nn.Module):
    """
    Mixture of Experts Layer
    
    KEY COMPONENTS:
    1. Multiple experts (feed-forward networks)
    2. Router (gating network)
    3. Top-k routing (select k experts)
    4. Weighted combination of expert outputs
    
    EFFICIENCY:
    - Total parameters: num_experts × params_per_expert
    - Active parameters: k × params_per_expert
    - Only k experts compute per token
    """
    def __init__(self, d_model: int, d_ff: int, num_experts: int, 
                 top_k: int = 2, dropout: float = 0.1):
        super().__init__()
        self.d_model = d_model
        self.num_experts = num_experts
        self.top_k = top_k
        
        # Router
        self.router = Router(d_model, num_experts)
        
        # Experts
        self.experts = nn.ModuleList([
            Expert(d_model, d_ff, dropout)
            for _ in range(num_experts)
        ])
    
    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, dict]:
        """
        Forward pass with MoE
        
        Args:
            x: Input, shape (batch, seq_len, d_model)
        Returns:
            output: MoE output, shape (batch, seq_len, d_model)
            routing_info: Dictionary with routing statistics
        """
        batch_size, seq_len, d_model = x.shape
        
        # Step 1: Compute router scores
        router_logits = self.router(x)  # (batch, seq_len, num_experts)
        router_probs = F.softmax(router_logits, dim=-1)
        
        # Step 2: Top-k routing
        # Select k experts with highest scores
        top_k_probs, top_k_indices = torch.topk(router_probs, self.top_k, dim=-1)
        # top_k_probs: (batch, seq_len, k)
        # top_k_indices: (batch, seq_len, k)
        
        # Step 3: Renormalize top-k probabilities
        top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)
        
        # Step 4: Process through selected experts
        # Flatten for processing
        x_flat = x.view(-1, d_model)  # (batch * seq_len, d_model)
        top_k_indices_flat = top_k_indices.view(-1, self.top_k)  # (batch * seq_len, k)
        top_k_probs_flat = top_k_probs.view(-1, self.top_k)  # (batch * seq_len, k)
        
        # Initialize output
        output_flat = torch.zeros_like(x_flat)
        
        # Process each position
        for i in range(batch_size * seq_len):
            expert_indices = top_k_indices_flat[i]  # (k,)
            expert_probs = top_k_probs_flat[i]  # (k,)
            
            # Weighted combination of expert outputs
            for j, expert_idx in enumerate(expert_indices):
                expert_output = self.experts[expert_idx](x_flat[i:i+1])
                output_flat[i] += expert_probs[j] * expert_output.squeeze(0)
        
        # Reshape back
        output = output_flat.view(batch_size, seq_len, d_model)
        
        # Compute routing statistics
        routing_info = {
            'expert_usage': self._compute_expert_usage(top_k_indices),
            'load_balance_loss': self._compute_load_balance_loss(router_probs)
        }
        
        return output, routing_info
    
    def _compute_expert_usage(self, top_k_indices: torch.Tensor) -> torch.Tensor:
        """
        Compute how many times each expert is used
        
        Returns: (num_experts,) tensor with usage counts
        """
        usage = torch.zeros(self.num_experts, device=top_k_indices.device)
        for idx in top_k_indices.flatten():
            usage[idx] += 1
        return usage / top_k_indices.numel()  # Normalize
    
    def _compute_load_balance_loss(self, router_probs: torch.Tensor) -> torch.Tensor:
        """
        Compute load balancing loss
        
        Encourages uniform expert usage
        L = (1/num_experts) * sum(load_i)²
        
        Where load_i is fraction of tokens routed to expert i
        """
        # Average router probabilities across batch and sequence
        avg_probs = router_probs.mean(dim=(0, 1))  # (num_experts,)
        
        # Load balancing loss: encourage uniform distribution
        # Variance of expert usage (want low variance = uniform)
        load_balance_loss = torch.var(avg_probs)
        
        return load_balance_loss


# ==================== EFFICIENT MOE (VECTORIZED) ====================

class EfficientMoE(nn.Module):
    """
    More efficient MoE implementation
    
    Uses vectorized operations instead of loops
    """
    def __init__(self, d_model: int, d_ff: int, num_experts: int,
                 top_k: int = 2, dropout: float = 0.1):
        super().__init__()
        self.d_model = d_model
        self.num_experts = num_experts
        self.top_k = top_k
        
        self.router = Router(d_model, num_experts)
        self.experts = nn.ModuleList([
            Expert(d_model, d_ff, dropout)
            for _ in range(num_experts)
        ])
    
    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, dict]:
        """
        Efficient forward pass (vectorized)
        """
        batch_size, seq_len, d_model = x.shape
        
        # Router scores
        router_logits = self.router(x)  # (batch, seq_len, num_experts)
        router_probs = F.softmax(router_logits, dim=-1)
        
        # Top-k
        top_k_probs, top_k_indices = torch.topk(router_probs, self.top_k, dim=-1)
        top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)
        
        # Flatten
        x_flat = x.view(-1, d_model)  # (batch * seq_len, d_model)
        top_k_indices_flat = top_k_indices.view(-1, self.top_k)  # (batch * seq_len, k)
        top_k_probs_flat = top_k_probs.view(-1, self.top_k)  # (batch * seq_len, k)
        
        # Process all experts (inefficient but simpler)
        # In practice, would only process selected experts
        expert_outputs = torch.stack([
            expert(x_flat) for expert in self.experts
        ], dim=1)  # (batch * seq_len, num_experts, d_model)
        
        # Select and weight
        batch_indices = torch.arange(batch_size * seq_len, device=x.device)
        selected_outputs = expert_outputs[batch_indices.unsqueeze(1), top_k_indices_flat]
        # (batch * seq_len, k, d_model)
        
        # Weighted combination
        output_flat = (selected_outputs * top_k_probs_flat.unsqueeze(-1)).sum(dim=1)
        # (batch * seq_len, d_model)
        
        output = output_flat.view(batch_size, seq_len, d_model)
        
        routing_info = {
            'load_balance_loss': torch.var(router_probs.mean(dim=(0, 1)))
        }
        
        return output, routing_info


# ==================== SWITCH ROUTING (k=1) ====================

class SwitchMoE(nn.Module):
    """
    Switch Transformer style MoE (k=1)
    
    Always activates exactly 1 expert
    Maximum sparsity
    """
    def __init__(self, d_model: int, d_ff: int, num_experts: int, dropout: float = 0.1):
        super().__init__()
        self.moe = MixtureOfExperts(d_model, d_ff, num_experts, top_k=1, dropout=dropout)
    
    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, dict]:
        """
        Switch routing: always k=1
        """
        return self.moe(x)


# ==================== COMPARISON ====================

def compare_moe_vs_dense(d_model: int = 768, d_ff: int = 3072,
                         num_experts: int = 8, top_k: int = 2):
    """
    Compare MoE vs Dense model
    """
    print("=" * 80)
    print("MoE vs Dense Model Comparison")
    print("=" * 80)
    
    # Dense model parameters
    dense_params = 2 * d_model * d_ff  # FFN: d_model → d_ff → d_model
    
    # MoE model parameters
    expert_params = 2 * d_model * d_ff  # Per expert
    router_params = d_model * num_experts  # Router
    moe_total_params = num_experts * expert_params + router_params
    moe_active_params = top_k * expert_params + router_params
    
    print(f"\nConfiguration:")
    print(f"  d_model: {d_model}")
    print(f"  d_ff: {d_ff}")
    print(f"  num_experts: {num_experts}")
    print(f"  top_k: {top_k}")
    
    print("\n" + "-" * 80)
    print("PARAMETERS")
    print("-" * 80)
    print(f"Dense:     {dense_params:>12,} parameters")
    print(f"MoE Total: {moe_total_params:>12,} parameters ({moe_total_params/dense_params:.1f}×)")
    print(f"MoE Active: {moe_active_params:>11,} parameters ({moe_active_params/dense_params:.1f}×)")
    
    print("\n" + "-" * 80)
    print("COMPUTATION (per token)")
    print("-" * 80)
    dense_flops = 2 * d_model * d_ff
    moe_active_flops = top_k * 2 * d_model * d_ff + d_model * num_experts
    print(f"Dense:     {dense_flops:>12,} FLOPs")
    print(f"MoE Active: {moe_active_flops:>11,} FLOPs ({moe_active_flops/dense_flops:.2f}×)")
    
    print("\n" + "-" * 80)
    print("EFFICIENCY")
    print("-" * 80)
    print(f"MoE has {moe_total_params/dense_params:.1f}× more parameters")
    print(f"But only uses {moe_active_params/dense_params:.1f}× for computation")
    print(f"Efficiency: {moe_total_params/moe_active_params:.1f}× parameter efficiency")


# ==================== USAGE EXAMPLE ====================

if __name__ == "__main__":
    print("Mixture of Experts Implementation")
    print("=" * 80)
    
    # Comparison
    compare_moe_vs_dense(d_model=768, d_ff=3072, num_experts=8, top_k=2)
    
    print("\n\n" + "=" * 80)
    print("Example: MoE Layer")
    print("=" * 80)
    
    d_model = 768
    d_ff = 3072
    num_experts = 8
    top_k = 2
    
    moe = MixtureOfExperts(d_model, d_ff, num_experts, top_k)
    
    batch_size = 2
    seq_len = 10
    x = torch.randn(batch_size, seq_len, d_model)
    
    output, routing_info = moe(x)
    
    print(f"\nInput shape: {x.shape}")
    print(f"Output shape: {output.shape}")
    print(f"Expert usage: {routing_info['expert_usage']}")
    print(f"Load balance loss: {routing_info['load_balance_loss']:.4f}")
    
    print("\n" + "=" * 80)
    print("KEY INSIGHTS")
    print("=" * 80)
    print("""
    1. MoE enables models with many parameters (trillions)
    2. But only activates subset per token (efficient)
    3. Router decides which experts to use
    4. Load balancing ensures all experts are utilized
    5. Used in GPT-4, Mixtral-8x7B for efficiency
    """)