Attention ist der Kern von Transformern. Das Modell "achtet auf" (pays attention to) die relevantesten Teile der Eingabe.
Scaled Dot-Product Attention (Basis)
Das ist die fundamentale Operation hinter allem.
Mathematik
Attention(Q, K, V) = softmax(Q * K^T / sqrt(d_k)) * V
wobei:
- Q (Query) = "Was such ich?"
- K (Key) = "Worum geht es hier?"
- V (Value) = "Wie wichtig ist dieser Part?"
- d_k = Dimensions-Größe der Keys
- sqrt(d_k) = Skalierung (verhindert exploding softmax)
Praktisches Beispiel
Satz: "Das Katze sitzt auf Matte"
Punkt 1: Token "Katze"
Query (Katze): "Was modifiziert mich?"
Berechne Ähnlichkeit zu jedem Token:
- "Das": 0.1 (low)
- "sitzt": 0.7 (high!) ← "Katze sitzt"
- "auf": 0.5 (medium)
- "Matte": 0.3 (low)
Softmax Gewichte: [0.1, 0.7, 0.5, 0.3] → normalisiert
Output: Gewichtete Kombination aller Werte
Code-Beispiel
import torch
import torch.nn.functional as F
def scaled_dot_product_attention(Q, K, V):
"""
Q, K, V Shape: (batch, seq_len, d_k)
"""
d_k = Q.shape[-1]
# 1. Berechne Ähnlichkeiten
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# Shape: (batch, seq_len, seq_len)
# 2. Normalisiere mit Softmax
attention_weights = F.softmax(scores, dim=-1)
# Shape: (batch, seq_len, seq_len)
# 3. Wende auf Values an
output = torch.matmul(attention_weights, V)
# Shape: (batch, seq_len, d_k)
return output, attention_weights
Self-Attention
Self-Attention bedeutet: Das Token achtet auf sich selbst und andere Tokens in der gleichen Sequenz.
Der Unterschied
Self-Attention: Token achtet auf andere Tokens im gleichen Satz
Eingabe: "Das Katze sitzt auf"
Token "sitzt" kann alle Tokens sehen
Cross-Attention: Token aus Sequenz A achtet auf Sequenz B
Beispiel: Image Caption
Q = Bild-Features
K, V = Satz-Tokens
Causality (Kausale Attention)
In Sprach-Modellen (GPT) darf ein Token nur auf frühere Tokens achten, nicht auf zukünftige.
Veränderung: Masking
Token 0 kann sehen: [0]
Token 1 kann sehen: [0, 1]
Token 2 kann sehen: [0, 1, 2] ← (nicht 3, 4, 5!)
Masking-Code:
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
scores.masked_fill(mask, -float('inf'))
Multi-Head Attention
Ein Head allein ist begrenzt. Multi-Head ermöglicht mehrere "Perspektiven" gleichzeitig.
Konzept
Eingabe (512-dim)
├─ Head 1 (64-dim): Achtet auf "Subjekt-Verb" Beziehungen
├─ Head 2 (64-dim): Achtet auf "Zeitliche Struktur"
├─ Head 3 (64-dim): Achtet auf "Abhängigkeiten"
└─ Head 4 (64-dim): Achtet auf "Tokens weit entfernt"
(8 Heads insgesamt)
Alle Heads kombinieren → Finale Output
Implementation
class MultiHeadAttention(nn.Module):
def __init__(self, d_model=512, num_heads=8):
super().__init__()
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads # 512 / 8 = 64
# Linear Transformationen
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model) # Output
def forward(self, Q, K, V):
batch_size = Q.shape[0]
# 1. Linear Transformationen
Q = self.W_q(Q) # (batch, seq, 512)
K = self.W_k(K)
V = self.W_v(V)
# 2. Split in Multiple Heads
Q = Q.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# (batch, 8, seq, 64)
K = K.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
# 3. Scaled Dot-Product Attention für jeden Head
output, _ = scaled_dot_product_attention(Q, K, V)
# (batch, 8, seq, 64)
# 4. Kombiniere Heads
output = output.transpose(1, 2).contiguous()
# (batch, seq, 8, 64)
output = output.view(batch_size, -1, self.d_model)
# (batch, seq, 512)
# 5. Final Linear
output = self.W_o(output)
return output
Cross-Attention
Encoder-Decoder-Aufmerksamkeit: Ein Sequence achtet auf eine andere.
Anwendungen
1. Text-to-Image (Stable Diffusion)
Q = Bild-Features (was ich generiere)
K, V = Text-Embedding (worüber ich lese)
2. Machine Translation
Q = Target Language (was ich schreibe)
K, V = Source Language (was ich übersetze)
3. Question Answering
Q = Question
K, V = Dokument
Code-Beispiel (Stable Diffusion)
# Im U-Net Diffusion Model
class CrossAttentionBlock(nn.Module):
def __init__(self, hidden_dim=768):
super().__init__()
self.mha = MultiHeadAttention(hidden_dim)
def forward(self, x, context):
"""
x (image features): (batch, height*width, 768)
context (text): (batch, max_tokens, 768) # von CLIP
"""
# Q kommt vom Bild, K,V vom Text
attn_output = self.mha(
Q=x, # Bild: "Was soll ich zeichnen?"
K=context, # Text: "Ein Hund sitzt"
V=context # Text: "Ein Hund sitzt"
)
return x + attn_output
Flash Attention
Flash Attention ist eine Optimierung der Standard Attention, die schneller und speicherhungriger ist (Dao et al. 2022).
Problem der Standard Attention
Speicher-Bottleneck:
Q*K^T berechnet: (seq_len, seq_len) Matrix
Mit seq_len=4096: 4096 * 4096 = 16M Einträge
Bei float32: ~64 MB nur für diese Matrix
Bei transformer.forward mit vielen Layers:
Speicher explodiert → OOM (Out of Memory)
Flash Attention Lösung
Idee: Mach Attention in Blöcken statt global
Standard: seq_len=4096
Q,K,V: (4096, 64)
Berechne: 4096*4096 Attention Matrix
Flash: Teile in Blöcke: Block_size=128
Berechne: 128*128 statt 4096*4096
Loop über Blöcke, akkumuliere Ergebnisse
Speicher: 64x weniger (bei 4096)
Speed: 2-4x schneller
Impact in der Praxis
Beispiel: Transformers ohne Flash Attention
Seq_len = 2048, Batch = 8, Heads = 12
→ OOM bei Standard GPU (24GB VRAM)
Mit Flash Attention:
→ Läuft problemlos
→ 2-3x schneller
→ Ermöglicht längere Kontexte
Verwendung
# Automatisch in torch 2.0+
import torch
torch.nn.functional.scaled_dot_product_attention # Nutzt Flash wenn verfügbar
# Oder explizit
from flash_attn import flash_attn_func
# In transformers Library
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
attn_implementation="flash_attention_2" # ← Flash Attention
)
Multi-Query Attention (MQA)
MQA reduziert die Anzahl der Key/Value Heads — schnellere Inference, weniger Speicher.
Unterschied zu Multi-Head
Standard Multi-Head Attention (8 Heads):
Q: 8 Heads × (seq_len, 64)
K: 8 Heads × (seq_len, 64) ← Viel Speicher!
V: 8 Heads × (seq_len, 64)
Multi-Query Attention:
Q: 8 Heads × (seq_len, 64)
K: 1 Head × (seq_len, 512) ← Geteilt über alle Queries!
V: 1 Head × (seq_len, 512)
Praktischer Effekt
Speed bei Inference:
Standard MHA: 100 Tokens/sec
MQA: 300 Tokens/sec (3x schneller!)
KV-Cache beim Generieren:
MHA: seq_len * num_heads * head_dim
MQA: seq_len * head_dim (8x kleiner!)
Wo es verwendet wird
- Google PaLM 2
- Meta LLaMA 2
- Zusammenfassung: "Cache ist klein, Inference ist schnell"
Grouped-Query Attention (GQA)
GQA ist ein Hybrid zwischen Multi-Head und Multi-Query.
Der Kompromiss
Multi-Head: 8 Q-Heads, 8 K-Heads, 8 V-Heads
→ Beste Qualität, höchster Speicher
Grouped-Query: 8 Q-Heads, 2 K-Heads, 2 V-Heads
→ Kompromiss: 4x Speicher + 4x Qualität
Multi-Query: 8 Q-Heads, 1 K-Head, 1 V-Head
→ Schnellste, aber schlechtere Qualität
Implementation
class GroupedQueryAttention(nn.Module):
def __init__(self, num_q_heads=8, num_kv_heads=2):
self.num_q_heads = num_q_heads
self.num_kv_heads = num_kv_heads
# Q: viele Heads
self.W_q = nn.Linear(d_model, d_model)
# K,V: weniger Heads
self.W_k = nn.Linear(d_model, d_model // 4) # 1/4 der Dims
self.W_v = nn.Linear(d_model, d_model // 4)
def forward(self, Q, K, V):
# Q: (batch, seq, d_model)
# K, V: (batch, seq, d_model // 4) ← Mehr komprimiert!
# Repeat K,V um in Multi-Head Format zu passen
# ...repeat logic...
Sliding Window Attention
Statt globaler Attention nur auf nahe Tokens schauen — für lange Sequenzen.
Motivation
Satz: "Der Mann mit dem Hut geht zur Schule und kauft einen Kaffee."
Standard Attention: "Der" achtet auf "kauft" (weit weg)
Sliding Window: "Der" achtet nur auf nächste 64 Tokens
→ Spart 95% Berechnung!
Implementation
def sliding_window_attention(Q, K, V, window_size=64):
seq_len = Q.shape[1]
# Erstelle Window-Mask
window_mask = torch.ones(seq_len, seq_len)
for i in range(seq_len):
# Token i kann nur Tokens [i-window_size, i+window_size] sehen
window_mask[i, :max(0, i-window_size)] = 0
window_mask[i, min(seq_len, i+window_size):] = 0
# Standard Attention mit Window-Mask
scores = torch.matmul(Q, K.transpose(-2, -1))
scores.masked_fill(~window_mask.bool(), -float('inf'))
attention = F.softmax(scores, dim=-1)
return torch.matmul(attention, V)
Wo verwendet
- Llama 2
- Mistral 7B (window_size=4096)
- Local Attention in Vision Transformers
Sparse Attention Patterns
Für sehr lange Sequenzen nur auf ausgewählte Tokens achten.
Pattern-Typen
1. Local Attention (Sliding Window)
Achtet nur auf nahe Tokens
2. Strided Attention
Achtet auf jeden n-ten Token
Pattern: [0, n, 2n, 3n, ...]
3. Fixed Attention
Token "achtet auf" bestimmte fixe Positionen
4. Longformer Pattern
Mischung aus Local + Global (für wichtige Tokens)
Vergleich
| Pattern | Komplexität | Qualität | Anwendung |
|---|---|---|---|
| Global | O(n²) | Beste | Standard |
| Sliding | O(n*w) | Gut | Lange Sequenzen |
| Strided | O(n²/k) | Mittel | Sehr lange Sequenzen |
| Fixed | O(n) | Mittel | Spezielle Patterns |
Attention Visualisierungen
Beispiel: Machine Translation
Satz: "Je suis heureux" → "I am happy"
Attention Matrix:
Je suis heureux
I 0.8 0.15 0.05
am 0.1 0.7 0.2
happy 0.05 0.15 0.8
Interpretation:
- "I" achtet hauptsächlich auf "Je" (Pronomen)
- "am" achtet auf "suis" (Verb)
- "happy" achtet auf "heureux" (Adjektiv)
Visualisierung in Transformers
from transformers import AutoTokenizer, AutoModel
model_name = "bert-base-uncased"
model = AutoModel.from_pretrained(model_name, output_attentions=True)
tokenizer = AutoTokenizer.from_pretrained(model_name)
text = "The cat sat on the mat"
inputs = tokenizer.encode(text, return_tensors="pt")
outputs = model(inputs)
attention = outputs[-1] # Attention weights
# attention shape: (layer, head, seq_len, seq_len)
# Visualisiere mit matplotlib
Performance-Tipps
1. Verwende Flash Attention wo möglich
# Automat in torch 2.0+
# Oder mit transformers:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
attn_implementation="flash_attention_2"
)
2. Länge begrenzen bei Inference
# Nicht: max_new_tokens=4096 bei Batch Inference
# Sondern: max_length=512
outputs = model.generate(
input_ids,
max_new_tokens=256, # Kürzer = schneller
do_sample=False # Deterministisch
)
3. KV-Cache nutzen
# Mit KV-Cache
outputs = model(input_ids, use_cache=True, past_key_values=None)
# Dann bei nächsten Token:
outputs = model(
next_token,
use_cache=True,
past_key_values=outputs.past_key_values # ← Cache reuse!
)
