Numerical Stability: Log-Sum-Exp, FP16 Pitfalls, NaN Hunting — Hidden Hours of LLM Training
Floating point representations (FP32, FP16, BF16, FP8), overflow/underflow/NaN hunting, log-sum-exp trick, softmax numerical stability, mixed precision training (autocast + GradScaler), numerical roots of pretrain loss spikes.
Şükrü Yusuf KAYA
40 min read
Intermediate🔢 Floating point'in karanlık tarafı
LLM mühendisinin günlük debug'larının %30'u sayısal sorunlar — NaN, Inf, loss spike, gradient explosion. Bu sorunların kökü çoğu zaman matematikte değil, floating point representation'da. Bu ders, bu görünmez katmanı görünür yapacak. 40 dakika sonra 'bu BF16 mı FP16 mı?' sorusuna bilinçli cevap vereceksin.
Ders Haritası#
- Floating point gerçeği: IEEE 754, mantissa, exponent
- FP32, FP16, BF16, FP8 karşılaştırması
- Overflow, underflow, NaN, Inf
- Log-sum-exp trick — softmax'ın sayısal kalbi
- Mixed precision training: autocast + GradScaler
- Pretrain loss spike'larının sayısal kökenleri
- Gradient clipping — coğrafi vs istatistiksel
- FP8 (Hopper, Blackwell) — yeni cephe
- Debug toolkit: hooks, anomaly detection
- LLM bağlamı: Llama 3 / DeepSeek-V3 mixed precision
1. Floating Point Gerçeği#
IEEE 754 standardına göre bir float üç parça:
- Sign (1 bit): pozitif/negatif
- Exponent (E bit): büyüklük (2'nin üssü)
- Mantissa (M bit): hassasiyet
Toplam:
(-1)^S × 1.mantissa × 2^(exponent - bias)Yaygın formatlar (2026)#
| Format | Toplam bit | Sign | Exp | Mantissa | Range | Hassasiyet | Donanım |
|---|---|---|---|---|---|---|---|
| FP64 | 64 | 1 | 11 | 52 | ±10³⁰⁸ | ~10⁻¹⁶ | Scientific |
| FP32 | 32 | 1 | 8 | 23 | ±10³⁸ | ~10⁻⁷ | Default deep learning |
| TF32 | 32→19 | 1 | 8 | 10 | ±10³⁸ | ~10⁻³ | Ampere+ default tensor core |
| FP16 | 16 | 1 | 5 | 10 | ±65,504 | ~10⁻³ | Volta+ tensor core |
| BF16 | 16 | 1 | 8 | 7 | ±10³⁸ | ~10⁻² | Ampere+ tensor core |
| FP8 E4M3 | 8 | 1 | 4 | 3 | ±448 | ~10⁻¹ | Hopper+ |
| FP8 E5M2 | 8 | 1 | 5 | 2 | ±57,344 | ~10⁻¹ | Hopper+ |
Anahtar gözlem: BF16 vs FP16#
BF16 ve FP16 ikisi de 16-bit. Fark: range vs precision trade-off.
- FP16: 10-bit mantissa (yüksek precision), 5-bit exponent (dar range). Range: ±65,504.
- BF16: 7-bit mantissa (düşük precision), 8-bit exponent (geniş range = FP32 ile aynı). Range: ±3.4e38.
LLM eğitiminde: range önemli (büyük gradient'ler), precision biraz kaybedilebilir. BF16 endüstri standardı.
FP16 ne zaman? Eski donanım (Volta V100), inference. Eğitimde FP16 kullanmıyoruz çünkü gradient overflow yaygın.
python
import torch # FP32 vs FP16 vs BF16 precision farkıx_fp32 = torch.tensor(1.0001, dtype=torch.float32)x_fp16 = torch.tensor(1.0001, dtype=torch.float16)x_bf16 = torch.tensor(1.0001, dtype=torch.bfloat16) print(f"FP32: {x_fp32.item():.10f}") # 1.0001000166print(f"FP16: {x_fp16.item():.10f}") # 1.0000976562 (precision lost)print(f"BF16: {x_bf16.item():.10f}") # 1.0000000000 (precision lost more) # Range farkıbig = torch.tensor(70000.0)print(f"FP32: {big.float()}") # 70000.0print(f"FP16: {big.half()}") # inf (max 65504)print(f"BF16: {big.bfloat16()}") # 70144.0 (precision kayıp ama range OK) # FP8 örneği (Hopper/H100 üzerinde)try: fp8_e4m3 = torch.tensor(1.0, dtype=torch.float8_e4m3fn) fp8_e5m2 = torch.tensor(1.0, dtype=torch.float8_e5m2) print("FP8 E4M3:", fp8_e4m3) print("FP8 E5M2:", fp8_e5m2)except Exception as e: print("FP8 unsupported on this hardware:", e)FP32, FP16, BF16, FP8 precision ve range farkları.
2. Overflow, Underflow, NaN, Inf#
Overflow (büyük sayı)#
x = 1e100 * 1e100 = 1e200InfUnderflow (sıfıra yaklaşma)#
x = 1e-100 * 1e-100 = 1e-200NaN (Not a Number)#
0/0Inf - Inf- ,
log(-1)sqrt(-1) 0 * Inf
NaN'in yayılma özelliği: . Bir parametre NaN olursa, tüm forward/backward NaN olur.
NaN + anything = NaNTespit#
x = some_tensor print(torch.isnan(x).any()) # True ise NaN var print(torch.isinf(x).any()) # True ise Inf var print(x.min(), x.max()) # range kontrol
python
import torch # NaN'ı erken yakalamadef assert_finite(x, name=""): if not torch.isfinite(x).all(): raise RuntimeError(f"{name} contains NaN/Inf!") # Training loop'a eklefor step in range(1000): # ... forward + backward + step ... for name, p in model.named_parameters(): try: assert_finite(p, f"param/{name}") if p.grad is not None: assert_finite(p.grad, f"grad/{name}") except RuntimeError as e: print(f"Step {step}: {e}") break # Alternatif: torch.autograd.set_detect_anomaly(True)# Backward'da NaN üreten ilk op'u yakalar (yavaş ama informative)torch.autograd.set_detect_anomaly(True)NaN/Inf erken tespit araçları.
3. Log-Sum-Exp Trick — Softmax'ın Sayısal Kalbi#
Softmax'ın naif implementasyonu:
def softmax_naive(x): return torch.exp(x) / torch.exp(x).sum()
Problem: 100 gibi büyük değerler içeriyorsa → FP32'de Inf. NaN sonuç.
xexp(100) ≈ 2.7e43Log-sum-exp trick#
Önce maximum'u çıkar:
x_i - me^{x_i - m}Cebirsel olarak eşdeğer (numerator ve denominator'a aynı sabiti böl).
Log-softmax için#
Bu da güvenli — log içinde Inf yok.
Cross-entropy + log-softmax birleştirme#
Hatırla (Ders 1.6): = log_softmax + nll_loss. Bu birleştirme tam olarak log-sum-exp trick kullanıyor.
F.cross_entropy(logits, target)Asla ayrı yazma — her zaman veya .
softmax + loglog_softmaxcross_entropypython
import torchimport torch.nn.functional as F # Naive softmax — overflowbig_logits = torch.tensor([100., 50., 200.])try: p = torch.exp(big_logits) / torch.exp(big_logits).sum() print("Naive:", p)except Exception as e: print("Naive error:", e)# tensor([0., 0., nan]) veya inf # Log-sum-exp trick (manuel)m = big_logits.max()p_safe = torch.exp(big_logits - m) / torch.exp(big_logits - m).sum()print("Safe:", p_safe)# tensor([1.9287e-44, 7.1746e-66, 1.0000e+00])# 200 token tüm probability'i alıyor (uniform değil) # PyTorch native (her zaman log-sum-exp trick kullanıyor)p_pytorch = F.softmax(big_logits, dim=0)print("PyTorch:", p_pytorch) # Log-softmaxlog_p = F.log_softmax(big_logits, dim=0)print("log-softmax:", log_p)# tensor([-100., -150., 0.]) — biraz lossy ama sane # Cross-entropytarget = torch.tensor([2])loss = F.cross_entropy(big_logits.unsqueeze(0), target)print("CE loss:", loss.item())# 0.0 — target'a 200 verildiği için doğru cevapLog-sum-exp trick'in pratik kullanımı.
4. Mixed Precision Training#
Modern LLM eğitiminde:
- Forward + backward: BF16 (veya FP16 — eski donanımda)
- Weight + optimizer states: FP32 (master copy)
- Loss accumulation: FP32
Bu, hız ve bellek tasarrufu ile sayısal stabiliteyi dengeleyen pratik.
PyTorch autocast#
autocastfrom torch.cuda.amp import autocast, GradScaler scaler = GradScaler() # FP16 için gerekli; BF16 için gerek yok for x, y in dataloader: optimizer.zero_grad() with autocast(dtype=torch.bfloat16): # BF16 forward pred = model(x) loss = criterion(pred, y) # BF16 ile direkt loss.backward() optimizer.step()
FP16 + GradScaler#
FP16 gradient'leri çok küçük olduğunda 0'a düşebilir (underflow). loss'u büyük bir scale ile çarpar, gradient'i FP32'ye çevirir, scale'i düşürür.
GradScalerscaler = GradScaler() for x, y in dataloader: optimizer.zero_grad() with autocast(dtype=torch.float16): pred = model(x) loss = criterion(pred, y) scaler.scale(loss).backward() # loss × scale scaler.step(optimizer) # gradient unscale + step scaler.update() # scale'i güncelle
BF16 — GradScaler gerek yok#
BF16 range FP32 ile aynı (8-bit exponent), underflow nadir → GradScaler gereksiz. Sadece autocast kullan.
5. Pretrain Loss Spike'larının Sayısal Kökenleri#
Llama 3 / DeepSeek-V3 / Qwen pretrain'inde loss spike (geçici büyük artış) yaygın. Sebepler:
1. Aktivasyon ya da gradient'in patlaması#
- LayerNorm yerine RMSNorm kullanılır → ε ile koruma ama hâlâ riskli
- Çözüm: gradient clipping (ise scale)
||g|| > max_norm
2. Loss surface'de keskin geçiş#
- Bir batch zorlu (out-of-distribution) örnekler içerebilir
- Model bir saddle'dan çıkıp çevreye dağılır → loss artar
- Çoğunlukla self-correcting
3. Adam optimizer state corruption#
- (second moment) çok küçük → effective lr çok büyük → patla
v_t - Çözüm: β₂'yi 0.95'e düşür (önceki ders), warmup uzun tut
4. Mixed precision underflow / overflow#
- FP16'da gradient → 0 (underflow), update lr × 0 = 0, model durduk
- Veya: gradient → inf (overflow), GradScaler skip step, ama bir parametre NaN olmuş
- Çözüm: BF16'ya geç
5. Data quality#
- Tek bir corrupted document loss'u patlatabilir
- Çözüm: filtering pipeline (Modül 15'te detay)
Debugging stratejisi#
# Loss'tan önce her N adımda gradient norm'u logla total_norm = 0 for p in model.parameters(): if p.grad is not None: total_norm += p.grad.norm() ** 2 total_norm = total_norm ** 0.5 wandb.log({"grad_norm": total_norm, "step": step}) # Spike'tan önce grad_norm anormal yükselir → erken uyarı
6. Gradient Clipping#
Gradient'in toplam normu bir threshold'u aşarsa scale et:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
Tipik max_norm değerleri#
- LLM pretrain: 1.0 (Llama, Qwen, DeepSeek)
- Fine-tuning: 0.5 (daha sıkı)
- Vision: 5.0-10.0 (daha esnek)
Per-parameter vs global#
Yukarıdaki global — tüm gradient'lerin toplam L2 normu. Alternatif: per-layer veya per-parameter clipping (nadir).
Adaptive clipping#
AGC (Brock 2021): ile 'yi karşılaştır. ise scale. Self-adapting. Bazı modern modellerde (Imagen) kullanılıyor.
||g||||θ||||g|| / ||θ|| > τ7. FP8 — Hopper ve Blackwell'in Yeni Cephesi#
NVIDIA H100 (Hopper, 2022) FP8 hardware desteği ekledi. Blackwell (B100/B200, 2025) FP8 native hızlandırmayı 2x'ledi.
Format#
İki çeşit:
- E4M3: 4-bit exponent, 3-bit mantissa. Range ±448, precision yüksek. Forward için.
- E5M2: 5-bit exponent, 2-bit mantissa. Range ±57K, precision düşük. Gradient için.
Yani FP8 = hybrid: forward'da E4M3, backward'da E5M2.
Avantajlar#
- 2x throughput (BF16'dan)
- 2x bellek tasarrufu
- Aktivasyonlar 8-bit, weight'ler 8-bit
Sorunlar#
- Scale management: her tensor için ayrı scale gerekli (her layer kendi range'inde olmalı)
- NVIDIA Transformer Engine library bunu handle ediyor ama complexity yüksek
- DeepSeek-V3 (2024) native FP8 training yaptı (paper'da detaylı) — büyük başarı
- Production'da hâlâ erken aşama
Kullanım#
# NVIDIA Transformer Engine ile import transformer_engine.pytorch as te # Standart Linear yerine linear = te.Linear(in_features=1024, out_features=4096) with te.fp8_autocast(enabled=True): out = linear(x)
Hangi modelde?#
- DeepSeek-V3 (671B) — native FP8 pretrain
- Llama 4 (rumored) — kısmi FP8
- Most LLMs — hâlâ BF16, FP8 inference'a doğru geçiyor
8. Debug Toolkit — NaN Avı#
torch.autograd.set_detect_anomaly(True)#
torch.autograd.set_detect_anomaly(True)Backward sırasında NaN üreten ilk operasyonu identify eder. Çok yavaş (10x slowdown) ama informative.
Forward hook'lar ile aktivasyon takibi#
def check_activation(module, input, output): if isinstance(output, torch.Tensor) and not torch.isfinite(output).all(): print(f"NaN/Inf in {module}: input range [{input[0].min():.4f}, {input[0].max():.4f}]") for name, module in model.named_modules(): if isinstance(module, (nn.Linear, nn.LayerNorm)): module.register_forward_hook(check_activation)
Weight ve gradient stats#
def log_stats(model, step): for name, p in model.named_parameters(): wandb.log({ f"weight/{name}/mean": p.data.mean().item(), f"weight/{name}/std": p.data.std().item(), f"weight/{name}/abs_max": p.data.abs().max().item(), }) if p.grad is not None: wandb.log({ f"grad/{name}/norm": p.grad.norm().item(), f"grad/{name}/abs_max": p.grad.abs().max().item(), })
Checkpoint sık aralarla#
NaN olunca son checkpoint'ten yeniden başlamak için. LLM pretrain'de her 1000-5000 step'te checkpoint.
9. Modern LLM Mixed Precision Config'leri#
Llama 3 (Meta)#
- BF16 her yerde
- GradScaler kullanılmıyor
- Master weights: BF16 (FP32 değil — küçük model olsa farklı olurdu, 70B+'da BF16 yeter)
- Gradient clipping: 1.0
- AdamW + linear warmup + cosine decay
DeepSeek-V3 (2024)#
- FP8 native (E4M3 forward, E5M2 backward)
- Master weights: FP32
- Optimizer states: BF16 (memory tasarrufu)
- Custom scaling per-block (NVIDIA TE library)
- "FP8 paper": detaylı mühendislik (DeepSeek tech report)
Qwen 2.5 (Alibaba)#
- BF16 standart
- Bazı küçük modelde (0.5B, 1.5B) FP16 + GradScaler
- Gradient clipping: 1.0
Mistral / Llama 4#
- BF16 + FP8 hybrid (gelecek)
- Hopper/Blackwell optimize
Pratik takeaway#
2026 LLM pretrain default'u: BF16 + AdamW + warmup + cosine + grad_clip(1.0). Frontier (DeepSeek-V3) FP8 ekliyor. FP16'yı sadece eski donanımda zorla.
10. Mini Egzersizler#
-
FP16 vs BF16 hangisi: Pretrain için H100'üm var. FP16 mı BF16 mı tercih edeyim? Neden?
-
Log-sum-exp manual: 5 değerli bir vektörle softmax'ı (a) naif yöntemle, (b) LSE trick ile hesapla. Sonuç aynı mı?
-
GradScaler ne yapıyor: FP16'da loss = 0.001, gradient = 1e-6 oluyor. FP16 hassasiyetinde gradient nasıl korunur? GradScaler'ın scale factor'ü nasıl seçilir?
-
Loss spike root cause: Pretrain'de step 10000'de loss 2.5'tan 8.0'a sıçradı. Şüpheli sebepleri sırala ve hangi metrikleri loglarsın?
-
FP8 trade-off: FP8 ile BF16'dan 2x hızlı eğitim mümkün. Pratik adoption neden yavaş?
Bu Derste Neler Öğrendik?#
✓ IEEE 754 floating point: sign + exponent + mantissa
✓ FP32 vs FP16 vs BF16 vs FP8: range/precision trade-off'ları
✓ BF16 modern LLM standardı (FP16 değil)
✓ Overflow, underflow, NaN/Inf yayılımı
✓ Log-sum-exp trick — softmax'ın temeli
✓ Mixed precision: autocast (BF16) + GradScaler (FP16)
✓ Pretrain loss spike'ların 5 sebebi ve debug
✓ Gradient clipping — max_norm=1.0 default
✓ FP8 (Hopper/Blackwell) — DeepSeek-V3 adoption
✓ Debug toolkit: anomaly detection, hooks, stats
Modül 1 sonuna geldik#
Toplam 9 ders, ~350 dakika içerik. Lineer cebir, matris ayrıştırması, kalkulus, autograd, olasılık, MLE/MAP, bilgi teorisi, optimizasyon, numerik stabilite — bir LLM mühendisinin matematik cephaneliği tamam.
Sıradaki Modül#
1.10 — Bilgi Geometrisi ve Manifold Sezgisi (Modül 1'in son dersi)
Embedding space'in neden anlamlı olduğu, Riemannian geometri sezgisi, t-SNE/UMAP, Fisher information, natural gradient. Modül 2'ye geçişte bağ.
Modül 2 — PyTorch'tan Önce — NumPy ve Otomatik Türev Sıfırdan
Karpathy micrograd Türkçe (1.4'ten genişletilmiş), tinygrad tarzı tensor autograd lab, computational graph'i sıfırdan yazma.
Frequently Asked Questions
For training: BF16 > FP16 (range advantage). For inference: depends. (1) **BF16 usually suffices**. (2) **Volta GPU (V100)** is FP16 native, no BF16 — V100 forces FP16. (3) **Edge/mobile**: some NPUs only support FP16. (4) **Specific layers**: sometimes FP16 gives more precision (e.g., low-temp softmax). Practical: BF16 default, FP16 only for hardware constraints.
Yorumlar & Soru-Cevap
(0)Yorum yazmak için giriş yap.
Yorumlar yükleniyor...
Related Content
Module 0: Course Framework & Workshop Setup
Who Is an LLM Engineer? The AI Engineering Career Ladder from Junior to Staff
Start LearningModule 0: Course Framework & Workshop Setup
Course Philosophy: Why This Path, Why This Order — The Skeleton of an 8-Month Curriculum
Start LearningModule 0: Course Framework & Workshop Setup