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"
- Mixture of Depths: Dynamically Allocating Compute in Transformer Language Models
- DeepSeek-V3 Technical Report
- Adaptive Computation in Transformers
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