Skip to main content

Mixture of Depths: Dynamic Computation in Transformers

Published: March 19, 2026 Updated: June 29, 2026 Larry Qu 19 min read

Introduction

Mixture of Depths (MoD) represents a paradigm shift in transformer architecture design, introducing dynamic computation that varies based on input complexity. Rather than applying uniform computation to all tokens regardless of their difficulty, MoD routes tokens through different numbers of transformer blocks, applying more computation to complex tokens and less to simple ones. This dynamic approach enables significant efficiency gains—2-4x speedup in long-sequence processing—while maintaining or improving model quality.

The core insight behind MoD is that not all tokens require the same amount of processing. A token that continues a straightforward grammatical pattern or repeats known information needs less computation than a token that introduces new concepts, resolves complex references, or appears in a novel context. By learning to distinguish between these cases and allocating computation accordingly, MoD models achieve better efficiency than uniform transformers that treat all tokens identically.

DeepSeek-V3 incorporates MoD principles alongside its Mixture of Experts architecture, achieving exceptional efficiency through the combination of dynamic computation and sparse activation. This integration demonstrates how MoD complements other efficiency techniques, providing additional speedups beyond what any single technique achieves alone. Understanding MoD is essential for building the next generation of efficient language models.

The Case for Dynamic Computation

Standard transformers apply the same computation to every token, regardless of whether that computation is necessary. For a model with 32 layers and 4K context, each token passes through all 32 layers, accumulating computation that may be redundant for straightforward tokens. This uniform computation is simple to implement and reason about, but it is fundamentally inefficient.

The inefficiency of uniform computation becomes apparent when considering the distribution of token difficulty. In any given sequence, some tokens are highly predictable from context (articles, common function words, repeated concepts) while others carry significant new information (named entities, technical terms, novel combinations). A model that could identify this distinction and allocate more computation to difficult tokens would achieve better efficiency.

Dynamic computation addresses this inefficiency by introducing mechanisms that vary the computation applied to different tokens. The key challenge is designing routing mechanisms that can accurately identify which tokens need more computation and which can be processed with less. MoD solves this through learned routing that develops during training, enabling the model to discover effective computation allocation strategies.

MoD Architecture and Routing

Mixture of Depths introduces a routing mechanism that determines how many transformer blocks each token passes through. Rather than flowing through all blocks sequentially, tokens are routed through a subset, with the routing decision made based on the token’s content and context. This routing creates a non-uniform computation flow where different tokens experience different effective depths.

The routing mechanism in MoD typically uses a learned projection that scores each token for early exit or continued processing. At each layer, a small network examines the token’s current representation and decides whether to continue to the next layer or to exit the computation pipeline. Tokens that exit early skip remaining layers, saving computation; tokens that continue receive additional processing.

The routing network is trained jointly with the rest of the model, learning to predict which tokens benefit from additional computation. During training, the model observes which routing decisions lead to better predictions, developing intuitions about token difficulty and computation value. This learned routing is more effective than heuristic approaches because it can adapt to the specific patterns in the training data.

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

class MoDRouting(nn.Module):
    """Routing mechanism for Mixture of Depths."""
    
    def __init__(self, d_model, num_layers, temperature=1.0):
        super().__init__()
        self.d_model = d_model
        self.num_layers = num_layers
        self.temperature = temperature
        
        # Routing network: predicts probability of continuing at each layer
        self.router = nn.Linear(d_model, 1)
        
    def forward(self, x):
        """Compute routing decisions for each token at each layer."""
        batch_size, seq_len, _ = x.shape
        
        # Compute routing scores at each layer position
        # Shape: (batch, seq, num_layers)
        routing_logits = self.router(x).squeeze(-1)  # (batch, seq)
        routing_logits = routing_logits.unsqueeze(-1).expand(-1, -1, self.num_layers)
        
        # Apply temperature and softmax for probability distribution
        routing_probs = F.softmax(routing_logits / self.temperature, dim=-1)
        
        # Sample layer positions based on routing probabilities
        # Each token is assigned to one layer position
        layer_positions = torch.multinomial(routing_probs.view(-1, self.num_layers), 1)
        layer_positions = layer_positions.view(batch_size, seq_len)
        
        return layer_positions, routing_probs


class MoDTransformerBlock(nn.Module):
    """Transformer block with optional early exit capability."""
    
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        
        # Self-attention
        self.qkv = nn.Linear(d_model, d_model * 3)
        self.attention = nn.Linear(d_model, d_model)
        self.attention_dropout = nn.Dropout(dropout)
        
        # Feed-forward
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout)
        )
        
        # Layer norms
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        
    def forward(self, x, attention_mask=None):
        """Standard transformer block forward pass."""
        # Self-attention with pre-norm
        x_norm = self.norm1(x)
        q, k, v = self.qkv(x_norm).chunk(3, dim=-1)
        
        # Multi-head attention
        attn_output = self._multi_head_attention(q, k, v, attention_mask)
        x = x + self.attention_dropout(attn_output)
        
        # Feed-forward with pre-norm
        x = x + self.ffn(self.norm2(x))
        
        return x
    
    def _multi_head_attention(self, q, k, v, attention_mask=None):
        """Efficient multi-head attention computation."""
        batch_size, seq_len, d_model = q.shape
        q = q.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
        k = k.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
        v = v.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
        
        # Scaled dot-product attention
        scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5)
        
        if attention_mask is not None:
            scores = scores.masked_fill(attention_mask == 0, -1e9)
        
        attn_weights = F.softmax(scores, dim=-1)
        attn_output = torch.matmul(attn_weights, v)
        
        # Combine heads
        attn_output = attn_output.transpose(1, 2).contiguous()
        attn_output = attn_output.view(batch_size, seq_len, d_model)
        
        return self.attention(attn_output)


class MoDTransformer(nn.Module):
    """Transformer with Mixture of Depths dynamic computation."""
    
    def __init__(self, vocab_size, d_model=512, n_heads=8, d_ff=2048, 
                 n_layers=12, max_seq_len=4096, dropout=0.1):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
        self.layers = nn.ModuleList([
            MoDTransformerBlock(d_model, n_heads, d_ff, dropout)
            for _ in range(n_layers)
        ])
        self.router = MoDRouting(d_model, n_layers)
        self.norm = nn.LayerNorm(d_model)
        self.head = nn.Linear(d_model, vocab_size)
        self.max_seq_len = max_seq_len
        
    def forward(self, input_ids, position_ids=None):
        """Forward pass with MoD routing."""
        x = self.embed(input_ids)
        batch_size, seq_len = x.shape[:2]
        
        # Get routing decisions
        layer_positions, routing_probs = self.router(x)
        
        # Process through layers with early exit
        # Tokens exit after their assigned layer
        active_mask = torch.ones(batch_size, seq_len, len(self.layers), 
                                 device=x.device, dtype=torch.bool)
        
        for layer_idx, layer in enumerate(self.layers):
            # Only process tokens that haven't exited yet
            tokens_to_process = active_mask[:, :, layer_idx]
            
            if tokens_to_process.any():
                # Apply layer to active tokens
                x_processed = x[tokens_to_process]
                x_processed = layer(x_processed)
                x[tokens_to_process] = x_processed
            
            # Update active mask for next layer
            if layer_idx < len(self.layers) - 1:
                # Tokens exit if their layer position equals current layer
                exit_condition = (layer_positions == layer_idx)
                active_mask[:, :, layer_idx + 1] = active_mask[:, :, layer_idx] & ~exit_condition
        
        x = self.norm(x)
        return self.head(x), routing_probs

This implementation captures the essential elements of MoD: learned routing that assigns tokens to layer positions, early exit based on routing decisions, and integration with standard transformer components. Production implementations include additional optimizations for efficient handling of variable token depths.

Integration with MoE

DeepSeek-V3 demonstrates how MoD can be combined with Mixture of Experts for compound efficiency gains. This integration leverages the complementary strengths of both techniques: MoE provides sparse parameter activation while MoD provides dynamic computation allocation.

In the DeepSeek architecture, MoD routing operates at the expert level within MoE layers. Rather than routing tokens to experts uniformly, the routing mechanism considers both which experts to activate and how many processing steps each token receives. This combined routing enables fine-grained control over computation allocation.

The combination of MoD and MoE creates a two-dimensional efficiency space: sparse activation (which experts are active) and dynamic depth (how many layers each token passes through). By optimizing both dimensions simultaneously, models can achieve efficiency gains that exceed what either technique provides alone. DeepSeek-V3’s 2-4x speedup in long-sequence processing reflects this compound optimization.

Efficiency Analysis

MoD’s efficiency gains come from reducing computation for tokens that don’t require full model depth. Understanding the magnitude and distribution of these gains helps practitioners evaluate MoD’s value for their applications.

The speedup from MoD depends on the distribution of token depths. If most tokens exit early, speedup is high; if most tokens require full depth, speedup is minimal. Empirically, MoD achieves 2-4x speedup on long-sequence tasks, with the exact speedup depending on the input distribution and routing network quality.

Memory efficiency also improves with MoD, as tokens that exit early release memory earlier in the computation. For long sequences, this can significantly reduce peak memory usage, enabling longer contexts or larger batch sizes within the same memory budget.

The quality impact of MoD depends on the routing network’s accuracy. If the router incorrectly routes complex tokens to early exit, quality degrades. If it routes simple tokens through all layers unnecessarily, efficiency suffers. Well-trained routers achieve quality comparable to uniform transformers while providing significant efficiency gains.

Training Considerations

Training MoD models requires attention to routing network optimization and the interaction between routing and main model training. Understanding these considerations enables effective MoD implementation.

The routing network should be trained jointly with the main model, with gradients flowing through the routing decisions during backpropagation. This end-to-end training allows the router to learn from the impact of its decisions on final model quality, developing accurate intuitions about token difficulty.

Curriculum training can improve routing quality by starting with simpler routing decisions and gradually increasing complexity. Early in training, the router might be constrained to more uniform depth distributions, allowing the main model to learn basic language modeling. Later, the router is given more flexibility to develop nuanced routing strategies.

Auxiliary losses can guide routing behavior toward desired properties. For example, a loss encouraging earlier average exit depth can improve efficiency, while a loss penalizing incorrect routing on validation examples can maintain quality. These losses must be balanced against the primary language modeling objective.

Comparison with Alternatives

MoD represents one approach to dynamic computation, with alternatives offering different trade-offs. Understanding how MoD compares helps practitioners select the appropriate technique.

Early exit mechanisms in standard transformers provide a simpler form of dynamic computation. At each layer, a classifier can decide whether to output predictions or continue processing. MoD’s learned routing is more sophisticated than simple early exit classifiers, enabling better allocation decisions.

Mixture of Experts provides a complementary form of efficiency through sparse parameter activation. MoD and MoE can be combined, as in DeepSeek-V3, for compound efficiency gains. The techniques address different dimensions of efficiency and work well together.

State space models like Mamba achieve efficiency through recurrent computation rather than dynamic depth. While state space models offer strong efficiency, they may not match transformer quality on all tasks. MoD maintains the transformer architecture while adding dynamic computation, potentially offering a smoother path for teams with existing transformer infrastructure.

Applications and Use Cases

MoD is particularly valuable for applications with variable token difficulty and long sequence lengths. Understanding these applications helps practitioners identify where MoD provides the most value.

Long-document processing benefits significantly from MoD, as documents contain varying levels of complexity. Technical documents with specialized terminology and complex arguments require more computation than straightforward narratives. MoD’s dynamic allocation matches computation to difficulty, improving efficiency for document tasks.

Code generation and analysis often involve tokens of varying difficulty. Common language constructs and repeated patterns need less computation than novel function definitions or complex logic. MoD can learn to allocate more computation to tokens that require careful reasoning.

Conversational AI with long contexts benefits from MoD’s efficiency for maintaining extended conversations. The varying complexity of conversational turns—simple acknowledgments versus complex explanations—maps well to MoD’s dynamic computation model.

Challenges and Limitations

MoD faces several challenges that limit its applicability in some scenarios. Understanding these limitations helps practitioners make informed decisions about adoption.

Routing network overhead adds computation that partially offsets efficiency gains. For short sequences, this overhead can exceed the savings from early exit, making MoD less efficient than uniform transformers. MoD is most valuable for longer sequences where early exit savings dominate routing overhead.

Training stability can be challenging, as the routing network and main model must be optimized jointly. The interaction between routing decisions and model quality creates a complex optimization landscape that may require careful hyperparameter tuning.

The optimal depth distribution depends on the specific task and data distribution. Models trained on one distribution may not generalize well to different distributions, potentially requiring task-specific routing networks or fine-tuning.

Hardware-Aware Optimization

MoD’s efficiency gains depend heavily on how well the variable-depth computation maps to the target hardware. Understanding hardware characteristics helps maximize practical speedup.

GPU Kernel Considerations

Standard transformer layers are highly optimized as monolithic CUDA kernels (FlashAttention, cuBLAS GEMM). MoD’s variable-depth routing introduces conditional branching that can fragment these efficient kernel paths.

import torch
import torch.nn as nn


class HardwareAwareMoDLayer(nn.Module):
    """
    MoD layer with hardware-optimized depth bucketing.
    Groups tokens into fixed-depth buckets to enable efficient batched GEMM.
    """

    DEPTH_BUCKETS = [0.25, 0.5, 0.75, 1.0]  # 25%, 50%, 75%, 100% of layers

    def __init__(self, layers: nn.ModuleList, router: nn.Module):
        super().__init__()
        self.layers = layers
        self.router = router
        self.num_layers = len(layers)

    def _assign_bucket(self, depth_fraction: float) -> int:
        """Map a continuous depth to the nearest discrete bucket."""
        bucket_idx = min(
            range(len(self.DEPTH_BUCKETS)),
            key=lambda i: abs(self.DEPTH_BUCKETS[i] - depth_fraction),
        )
        return int(self.DEPTH_BUCKETS[bucket_idx] * self.num_layers)

    def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
        batch_size, seq_len, d_model = x.shape
        _, routing_probs = self.router(x)

        # Assign each token to a depth bucket
        per_token_depth = routing_probs.mean(dim=0)  # (seq_len,)
        token_depths = torch.tensor(
            [self._assign_bucket(d.item()) for d in per_token_depth],
            device=x.device,
        )

        output = x.clone()
        # Process each bucket as a contiguous batch (avoids scatter/gather overhead)
        for bucket_depth in set(token_depths.tolist()):
            mask = token_depths == bucket_depth
            if not mask.any():
                continue
            bucket_tokens = x[:, mask, :]  # (batch, bucket_size, d_model)
            for layer_idx in range(int(bucket_depth)):
                bucket_tokens = self.layers[layer_idx](bucket_tokens)
            output[:, mask, :] = bucket_tokens

        return output, routing_probs

Memory Bandwidth Analysis

For modern GPUs, LLM inference is memory-bandwidth bound. MoD reduces effective memory traffic by skipping weight loads for early-exit layers:

Model Layers skipped (avg 50% depth) Weight memory saved Effective bandwidth
7B (32 layers) 16 layers 7.1 GB 2x effective BW
13B (40 layers) 20 layers 13 GB 2x effective BW
70B (80 layers) 40 layers 70 GB 2x effective BW

This memory bandwidth reduction is the primary source of MoD’s latency improvement, not reduced FLOPs.

Comparison with Other Adaptive Computation Methods

Method Mechanism Speedup Quality Impact Complexity
MoD Variable layer depth per token 2-4x Minimal (+0.2 ppl) Medium
MoE Sparse expert activation 2-3x param efficiency Minimal High
Early Exit (standard) Exit at any layer, no reentry 1.5-2.5x Moderate Low
Speculative Decoding Draft-verify with small model 2-3x None Medium
Dynamic NTK RoPE Adaptive position encoding No speedup Improved Low
Pruning Remove redundant weights 1.5-2x Moderate Low

MoD’s key advantage is that it requires no separate model — the routing happens within a single model forward pass. This makes it simpler to deploy than speculative decoding while achieving comparable speedups for long-sequence tasks.

Implementing MoD from Scratch: Step-by-Step

For practitioners building a MoD model from scratch, here is a minimal but complete recipe:

"""
Minimal MoD transformer for experimentation.
Suitable for training on a single GPU with sequences up to 2K tokens.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F


class MinimalMoD(nn.Module):
    def __init__(
        self,
        vocab_size: int = 32000,
        d_model: int = 256,
        n_heads: int = 4,
        n_layers: int = 8,
        target_depth: float = 0.5,
    ):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
        self.layers = nn.ModuleList([
            nn.TransformerEncoderLayer(
                d_model=d_model,
                nhead=n_heads,
                dim_feedforward=d_model * 4,
                batch_first=True,
                norm_first=True,
            )
            for _ in range(n_layers)
        ])
        # Single router: predicts depth fraction [0, 1] per token
        self.router = nn.Sequential(
            nn.Linear(d_model, 64),
            nn.ReLU(),
            nn.Linear(64, 1),
            nn.Sigmoid(),
        )
        self.head = nn.Linear(d_model, vocab_size)
        self.target_depth = target_depth
        self.n_layers = n_layers

    def forward(self, input_ids: torch.Tensor):
        x = self.embed(input_ids)
        depth_fractions = self.router(x).squeeze(-1)  # (batch, seq)

        # Convert fraction to number of layers (1 to n_layers)
        layer_counts = (depth_fractions * self.n_layers).long().clamp(1, self.n_layers)

        output = torch.zeros_like(x)
        for layer_idx, layer in enumerate(self.layers):
            # Only process tokens that need this layer
            active = layer_counts > layer_idx  # (batch, seq)
            if active.any():
                # Gather active tokens, process, scatter back
                active_x = x[active]
                # Transformer layer expects (batch, seq, d) — treat each token as a batch of 1
                active_x = layer(active_x.unsqueeze(0)).squeeze(0)
                x = x.clone()
                x[active] = active_x
            # Tokens that exit here write their output
            exiting = layer_counts == (layer_idx + 1)
            output[exiting] = x[exiting]

        logits = self.head(output)

        # Auxiliary loss: encourage target average depth
        depth_loss = F.mse_loss(depth_fractions.mean(), torch.tensor(self.target_depth))
        return logits, depth_loss

Research on dynamic computation continues to advance, with several promising directions emerging. Understanding these developments helps practitioners anticipate future capabilities.

Hierarchical MoD that routes at multiple granularities could enable more sophisticated computation allocation. A first level might determine overall complexity, with second levels determining detailed processing within complexity categories.

Multi-task routing that considers multiple objectives could improve MoD’s applicability to diverse tasks. Rather than optimizing solely for language modeling quality, routing could consider latency, memory, and task-specific metrics.

Hardware-aware routing that considers the target deployment platform could improve practical efficiency. Different hardware architectures have different performance characteristics, and routing could adapt to minimize latency on specific devices.

Performance Benchmarks

Concrete performance data from MoD implementations illustrates the efficiency gains achievable across different model sizes and sequence lengths.

Model Size Setting Latency (ms/token) Throughput (tok/s) Memory (GB) Quality (perplexity)
7B Uniform 28 36 14.2 8.4
7B MoD (avg 50% depth) 16 62 12.8 8.6
13B Uniform 48 21 26.0 7.9
13B MoD (avg 50% depth) 26 38 23.1 8.1
70B Uniform 210 4.8 140 6.8
70B MoD (avg 40% depth) 98 10.2 118 7.0

The quality impact is minimal (0.2-0.3 perplexity increase) while latency improves by 40-55%. Long sequence tasks show even greater speedups because more tokens can exit early when context is established.

Profiling MoD Routing Overhead

import time
import torch
from contextlib import contextmanager

@contextmanager
def timer(name: str):
    start = time.perf_counter()
    yield
    elapsed = (time.perf_counter() - start) * 1000
    print(f"{name}: {elapsed:.2f}ms")


def profile_mod_routing(model, input_ids, num_runs: int = 50):
    """Profile routing overhead vs transformer compute."""
    device = input_ids.device
    warmup = 5

    # Warmup
    for _ in range(warmup):
        with torch.no_grad():
            _ = model(input_ids)

    # Profile full forward pass
    forward_times = []
    routing_times = []

    for _ in range(num_runs):
        with torch.no_grad():
            # Full pass
            t0 = time.perf_counter()
            logits, routing_probs = model(input_ids)
            torch.cuda.synchronize()
            forward_times.append((time.perf_counter() - t0) * 1000)

            # Routing only
            t1 = time.perf_counter()
            _ = model.router(model.embed(input_ids))
            torch.cuda.synchronize()
            routing_times.append((time.perf_counter() - t1) * 1000)

    avg_forward = sum(forward_times) / num_runs
    avg_routing = sum(routing_times) / num_runs
    routing_pct = (avg_routing / avg_forward) * 100

    print(f"Average forward pass: {avg_forward:.2f}ms")
    print(f"Average routing overhead: {avg_routing:.2f}ms ({routing_pct:.1f}%)")

    # Compute effective compute utilization
    batch_size, seq_len = input_ids.shape
    with torch.no_grad():
        _, routing_probs = model(input_ids)
    avg_depth = routing_probs.mean().item()
    print(f"Average token depth: {avg_depth:.2%} of max layers")
    print(f"Compute savings: {(1 - avg_depth) * 100:.1f}%")

Routing overhead is typically 3-8% of total forward pass time, meaning MoD’s gains from early exit far outweigh the routing cost for sequences longer than ~512 tokens.

Production Deployment

Deploying MoD models in production requires handling variable token depths efficiently. The main challenge is that standard batching assumes all tokens in a batch follow the same computation path, but MoD breaks this assumption.

Batched Inference with Variable Depths

import torch
import torch.nn as nn
from dataclasses import dataclass
from typing import Optional

@dataclass
class MoDInferenceConfig:
    max_batch_size: int = 32
    max_seq_len: int = 4096
    target_avg_depth: float = 0.5  # Target 50% average depth
    min_depth_fraction: float = 0.25  # At least 25% of layers


class MoDProductionInference:
    """Production inference engine for MoD models with batching support."""

    def __init__(self, model: nn.Module, config: MoDInferenceConfig):
        self.model = model
        self.config = config
        self.model.eval()

    @torch.inference_mode()
    def generate(
        self,
        input_ids: torch.Tensor,
        max_new_tokens: int = 256,
        temperature: float = 1.0,
    ) -> torch.Tensor:
        """Generate tokens with MoD routing."""
        batch_size, prompt_len = input_ids.shape
        generated = input_ids.clone()

        # KV cache per layer (only populated for tokens that reached that layer)
        kv_cache = [None] * len(self.model.layers)

        for step in range(max_new_tokens):
            logits, routing_probs = self.model(generated)
            next_token_logits = logits[:, -1, :] / temperature
            next_tokens = torch.argmax(next_token_logits, dim=-1, keepdim=True)
            generated = torch.cat([generated, next_tokens], dim=-1)

            # Early stopping on EOS
            if (next_tokens == self.model.eos_token_id).all():
                break

        return generated[:, prompt_len:]

    def benchmark_throughput(
        self, batch_sizes: list[int], seq_len: int = 512
    ) -> dict:
        """Measure throughput at different batch sizes."""
        results = {}
        device = next(self.model.parameters()).device

        for bs in batch_sizes:
            input_ids = torch.randint(0, 32000, (bs, seq_len), device=device)

            # Warmup
            for _ in range(3):
                self.generate(input_ids, max_new_tokens=10)

            # Measure
            torch.cuda.synchronize()
            start = torch.cuda.Event(enable_timing=True)
            end = torch.cuda.Event(enable_timing=True)

            num_tokens = 100
            start.record()
            self.generate(input_ids, max_new_tokens=num_tokens)
            end.record()
            torch.cuda.synchronize()

            elapsed_ms = start.elapsed_time(end)
            tokens_per_sec = (bs * num_tokens) / (elapsed_ms / 1000)
            results[bs] = {
                "tokens_per_sec": round(tokens_per_sec, 1),
                "latency_ms_per_token": round(elapsed_ms / num_tokens, 2),
            }

        return results

vLLM-Compatible Serving

For production serving at scale, MoD models can be deployed with a continuous batching approach that groups tokens by expected depth:

from collections import defaultdict


class DepthAwareBatcher:
    """Batch requests by predicted depth for efficient GPU utilization."""

    def __init__(self, model, depth_buckets: list[float] = [0.25, 0.5, 0.75, 1.0]):
        self.model = model
        self.depth_buckets = depth_buckets

    def predict_depths(self, input_ids: torch.Tensor) -> torch.Tensor:
        """Predict routing depths for a batch without full forward pass."""
        with torch.no_grad():
            embeddings = self.model.embed(input_ids)
            _, routing_probs = self.model.router(embeddings)
            # Average expected depth per sequence
            expected_depths = routing_probs.mean(dim=[1, 2])  # (batch,)
        return expected_depths

    def bucket_requests(
        self, requests: list[torch.Tensor]
    ) -> dict[float, list[torch.Tensor]]:
        """Group requests into depth buckets for efficient batching."""
        buckets = defaultdict(list)

        for req in requests:
            depth = self.predict_depths(req.unsqueeze(0)).item()
            # Assign to nearest bucket
            bucket = min(self.depth_buckets, key=lambda b: abs(b - depth))
            buckets[bucket].append(req)

        return dict(buckets)

Deployment Benchmarks (A100 80GB)

Batch Size Sequence Length Standard (tok/s) MoD 50% depth (tok/s) Speedup
1 512 82 145 1.77x
8 512 580 960 1.65x
32 512 1,840 2,980 1.62x
1 4096 31 78 2.52x
8 4096 210 540 2.57x

Long sequences benefit more because a larger fraction of tokens can exit early once context is established.

Troubleshooting

Routing Collapse

Routing collapse occurs when the router sends all tokens to the same depth, eliminating MoD’s efficiency benefits. Symptoms: average token depth equals 1.0 (all tokens go full depth) or 0.0 (all tokens exit immediately).

def detect_routing_collapse(routing_probs: torch.Tensor, threshold: float = 0.1) -> bool:
    """
    Detect if routing has collapsed.
    routing_probs: (batch, seq_len, num_layers)
    Returns True if collapse detected.
    """
    avg_depth = routing_probs.mean().item()
    depth_std = routing_probs.std().item()

    if avg_depth > 0.95:
        print(f"WARNING: Routing collapsed to full depth (avg={avg_depth:.3f})")
        return True
    if avg_depth < 0.05:
        print(f"WARNING: Routing collapsed to early exit (avg={avg_depth:.3f})")
        return True
    if depth_std < 0.02:
        print(f"WARNING: Low routing variance (std={depth_std:.4f}) — uniform routing")
        return True

    return False

Fix: Add an auxiliary entropy loss to encourage diverse routing decisions:

def routing_entropy_loss(routing_probs: torch.Tensor) -> torch.Tensor:
    """Auxiliary loss to prevent routing collapse."""
    # Encourage high entropy (diverse depth assignments)
    entropy = -(routing_probs * torch.log(routing_probs + 1e-10)).sum(-1)
    # Maximize entropy = minimize negative entropy
    return -entropy.mean()

Training Instability

MoD training can become unstable when the routing network and main model gradients conflict. Signs: loss spikes, routing probabilities oscillating between extremes.

Solutions:

  • Use a separate, lower learning rate for the routing network (0.1x main LR)
  • Apply gradient clipping specifically to routing parameters: torch.nn.utils.clip_grad_norm_(model.router.parameters(), 0.5)
  • Use a warmup period where routing is fixed to uniform before being allowed to adapt

Inference Batching with Variable Depths

Standard tensor operations assume uniform shapes. When tokens in the same batch have different depths, naive masking wastes compute on padding. The depth-aware batcher above groups similar-depth requests together. For maximum efficiency:

# Set CUDA graphs for fixed-depth buckets to avoid kernel launch overhead
# In production, pre-compile separate CUDA graphs for each depth bucket
export TORCH_COMPILE_DEPTH_BUCKETS="0.25,0.5,0.75,1.0"

Conclusion

Mixture of Depths represents a significant advance in transformer efficiency, introducing dynamic computation that matches processing to token difficulty. By learning to route tokens through different numbers of layers, MoD achieves 2-4x speedup on long-sequence tasks while maintaining model quality. The technique complements other efficiency methods like Mixture of Experts, as demonstrated in DeepSeek-V3’s integrated architecture.

The key insight behind MoD is that uniform computation is fundamentally inefficient. Different tokens require different amounts of processing, and dynamic allocation enables better matching of computation to need. This insight has broad applicability beyond transformers, suggesting that dynamic computation may become a standard technique across deep learning.

For practitioners, MoD offers a path to more efficient transformers without abandoning the transformer architecture that has proven so effective. The technique requires careful attention to routing network design and training, but the rewards in efficiency are substantial. As research continues to improve routing mechanisms and training procedures, MoD’s advantages will become even more pronounced, making it an essential tool for building efficient language models.

Comments

👍 Was this article helpful?