Token Merging: Reducing Attention Compute by Merging Tokens¶
Overview¶
Token Merging (ToMe) reduces computation in transformer inference by merging similar tokens before attention, reducing sequence length. Achieves 2-4x speedup with minimal quality loss by removing redundant information.
- Paper: "Token Merging for Fast Stable Diffusion" (Bolya et al., 2023)
- Key Idea: Merge similar tokens before expensive attention computation
- Speedup: 2-4x with <2% quality loss
- Advantage: Works during inference without retraining
- Trade-off: Slight information loss from merging
The Problem: Sequence Length Drives Attention Cost¶
Attention Complexity¶
Attention computation cost:
Q @ K^T has complexity O(N²) where N = sequence length
Concrete example:
Model: LLaMA 7B
Sequence: 4096 tokens
Attention at each layer:
- Q shape: (32 heads, 4096, 128)
- K^T shape: (32 heads, 128, 4096)
- Output: (32 heads, 4096, 4096)
- Operations: 4096 × 4096 × 128 = 2.1B operations per layer
- 32 layers × 2.1B = 67B operations (just attention!)
- Takes ~50% of inference time!
Key insight:
Problem is O(N²), not the actual computations
If we reduce N from 4096 to 2048:
- New cost: 2048 × 2048 × 128 = 0.5B per layer
- Total: 8 × 0.5B = 16B (4x reduction!)
- Speedup: ~2-4x possible!
Question: Can we reduce sequence length without losing information?
Answer: Yes! Through token merging!
Why Merging is Feasible¶
Observation: Many tokens are redundant
Example: "The quick brown fox jumped over the lazy dog"
Token importance:
- "The" → common word, low information
- "quick" → descriptive, important
- "brown" → descriptive, important
- "fox" → main subject, very important
- "jumped" → action verb, very important
- "lazy" → descriptive, important
Redundancy:
- Multiple "the" are similar
- Adjacent descriptors can overlap
- Some tokens add little new information
Merging strategy:
- Identify similar tokens
- Merge their representations
- Reduce sequence length
- Attention now operates on merged sequence!
Result:
Original: [The, quick, brown, fox, jumped, over, the, lazy, dog]
Merged: [The+quick+brown, fox, jumped, over, lazy+dog]
- Reduced from 9 tokens to 6 (33% reduction)
How Token Merging Works¶
Core Algorithm¶
Input sequence: [t₁, t₂, t₃, t₄, t₅, t₆, t₇, t₈]
Step 1: Compute token similarity
- For each pair of adjacent tokens: compute cosine similarity
- Similarity matrix:
- t₁-t₂: 0.95 (very similar)
- t₂-t₃: 0.70 (moderately similar)
- t₃-t₄: 0.30 (dissimilar)
- t₄-t₅: 0.85 (similar)
- ...
Step 2: Decide which to merge
- Use bipartite matching or greedy algorithm
- Merge similar pairs
Step 3: Merge representations
- t₁-t₂ → (t₁ + t₂) / 2 (average representation)
- Keep t₃ (dissimilar to others)
- t₄-t₅ → (t₄ + t₅) / 2
- ...
Output: [merged(t₁,t₂), t₃, merged(t₄,t₅), t₆, t₇, t₈]
Reduced from 8 to 6 tokens!
Bipartite Matching Algorithm¶
def token_merge(hidden_states, reduction_ratio=0.5):
"""
Merge tokens using bipartite matching
Args:
hidden_states: (batch, seq_len, hidden_dim)
reduction_ratio: Target reduction (0.5 = keep 50% of tokens)
Returns:
merged_states: (batch, merged_len, hidden_dim)
indices: which tokens were kept/merged
"""
batch, seq_len, hidden_dim = hidden_states.shape
# Compute token similarity using cosine similarity
normalized = F.normalize(hidden_states, dim=-1) # (batch, seq_len, hidden_dim)
similarity = torch.matmul(normalized, normalized.transpose(-2, -1))
# similarity shape: (batch, seq_len, seq_len)
# Determine number of tokens to keep
keep_token_count = int(seq_len * reduction_ratio)
# Greedy bipartite matching
merged_states = []
merged_indices = []
used = set()
# Score each token by importance (max similarity to others)
token_scores = similarity.mean(dim=-1) # Average similarity
for i in range(seq_len):
if i in used:
continue
if len(merged_states) >= keep_token_count:
# Reached target number, merge remaining
merged_states.append(hidden_states[:, i:i+1, :])
used.add(i)
continue
# Find most similar unused token
best_match = None
best_similarity = -1
for j in range(i+1, seq_len):
if j not in used and similarity[:, i, j].mean() > best_similarity:
best_similarity = similarity[:, i, j].mean()
best_match = j
if best_match is not None and best_similarity > threshold:
# Merge i and best_match
merged = (hidden_states[:, i:i+1, :] +
hidden_states[:, best_match:best_match+1, :]) / 2
merged_states.append(merged)
used.add(i)
used.add(best_match)
else:
# Keep token alone
merged_states.append(hidden_states[:, i:i+1, :])
used.add(i)
# Concatenate merged states
merged = torch.cat(merged_states, dim=1) # (batch, merged_len, hidden_dim)
return merged
# Usage in transformer:
class TokenMergingTransformerLayer(nn.Module):
def __init__(self, hidden_dim, merge_ratio=0.5):
super().__init__()
self.merge_ratio = merge_ratio
self.attention = MultiHeadAttention(hidden_dim)
self.ffn = FeedForward(hidden_dim)
def forward(self, x):
# x: (batch, seq_len, hidden_dim)
# Merge tokens BEFORE attention (expensive operation)
x_merged = token_merge(x, self.merge_ratio)
# Attention on merged sequence (cheaper!)
attn_out = self.attention(x_merged)
# Unmerge or interpolate back to original length
# (implementation depends on tracking which tokens merged)
x_out = unmerge_tokens(attn_out, original_length=x.shape[1])
# FFN on original length
out = self.ffn(x_out)
return out
Merging Strategy¶
When to Merge¶
Option 1: Merge in early layers
- Merge in layer 1-10
- Early layers: more redundancy
- Later layers: more specific representations
- Strategy: Aggressive early merge (70% reduction)
Option 2: Merge progressively
- Layer 1: Merge 30% (keep 70% of tokens)
- Layer 5: Merge 30% (keep 70% of remaining)
- Layer 10: Merge 30% (keep ~34% of original)
- Strategy: Gradual reduction of sequence length
Option 3: Merge in middle layers
- Layers 1-8: No merge (preserve early context)
- Layers 9-16: Merge (reduce attention cost)
- Layers 17-32: No merge (final layers process merged)
Recommendation:
- Progressive merging works best
- 2-3% quality loss vs 3-5% with aggressive early merge
Similarity Threshold¶
Threshold determines what counts as "similar enough to merge"
High threshold (0.9):
- Only merge extremely similar tokens
- Fewer merges, less speedup
- Higher quality preservation
Low threshold (0.7):
- Merge more aggressively
- More speedup (larger sequence reduction)
- Higher quality loss
Typical: 0.75-0.85
- Balances speedup and quality
- ~2x speedup, <1% quality loss
Performance Impact¶
Speedup Analysis¶
Scenario: 4096 token sequence, LLaMA 7B
Standard inference:
- Attention computation: 50ms per layer × 32 = 1600ms
- FFN computation: 30ms per layer × 32 = 960ms
- Total: ~2.5 seconds per forward pass
- Throughput: 400 tokens/sec per GPU
With Token Merging (50% reduction):
- Merge 4096 → 2048 tokens (cost: ~5ms)
- Attention: 12ms per layer × 32 = 384ms (4x reduction!)
- FFN: 30ms per layer × 32 = 960ms (same, operates on 2048 anyway)
- Unmerge back: ~5ms
- Total: ~1.4 seconds
- Speedup: 1.8x (close to 2x expected from O(N²) reduction)
With aggressive merging (70% reduction):
- Merge 4096 → 1229 tokens
- Attention: 4.4ms per layer × 32 = 141ms (10x reduction!)
- Total: ~1.1 seconds
- Speedup: 2.3x
Quality Impact¶
Experiment: LLaMA 7B on MMLU benchmark
Reduction Speedup MMLU Score Quality Loss
───────────────────────────────────────────────
None 1x 45.3% 0%
30% (keep 70%) 1.3x 45.2% 0.2%
50% (keep 50%) 1.8x 45.0% 0.7%
70% (keep 30%) 2.3x 44.5% 1.8%
Observation:
- <1% quality loss for 2x speedup (excellent!)
- Progressive merging better than aggressive
- Works best with well-trained models
Different domains:
- Code: Slightly higher quality loss (1.0% at 2x)
- Creative: Lower quality loss (0.5% at 2x)
- Reasoning: Higher quality loss (1.5% at 2x)
- Merging removes information needed for reasoning
Unmerging Strategies¶
Simple Average¶
Merged output: [a', b', c'] (where a'=(a+b)/2)
Original: [a, b, c, d, ...]
Unmerge by duplication:
- a' → propagate to both a and b positions
- b' → propagate to both c position
- Result: Fill in original sequence length
Attention-Based Unmerging¶
Instead of simple duplication, use attention:
- Merged token a' represents both a and b
- Query: What should a and b be individually?
- Use attention to distribute a' back to a and b positions
- Smoother than simple duplication
Advantages and Limitations¶
Advantages¶
✅ 2-4x speedup without retraining
✅ Works during inference only
✅ Minimal quality loss (<1-2%)
✅ Simple to implement
✅ Compatible with other optimizations
✅ Effective for long sequences
Limitations¶
❌ Information loss from merging
❌ Quality degrades for complex reasoning
❌ Unmerging can be expensive if not careful
❌ Less effective on short sequences
❌ Sensitive to token similarity threshold
When to Use Token Merging¶
Use ToMe when:
✅ Long sequences (>2K tokens)
✅ Inference speed critical
✅ Slightly lower quality acceptable
✅ Attention cost is bottleneck
Avoid ToMe when:
❌ Short sequences (<512 tokens)
❌ Quality is paramount
❌ Complex reasoning (merging breaks it)
Key Takeaways¶
🔗 Merge similar tokens to reduce sequence length
⚡ O(N²) attention becomes O(M²) where M << N
📊 2x speedup with <1% quality loss typical
🎯 Progressive merging better than aggressive
📈 Most effective for long context inference
Related Notes¶
- Sliding Window Attention - Alternative efficiency approach
- 00 Attention Mechanisms - How merging affects attention
- Speculative Decoding - Different parallelization approach
- Llm Inference Optimization - Complete inference stack