Reproducibility Stack: Seeds, cuDNN Flags, and Deterministic CUDA — End the 'Works on My Machine' Problem
ML's most expensive time sink: irreproducible results. This lesson: seed management, cuDNN/cuBLAS deterministic flags, ATen non-deterministic op detection, dataloader worker seeding, cost of deterministic scatter/gather — all with practical code and real logs.
Şükrü Yusuf KAYA
32 min read
Intermediate🎯 Bu ders bittiğinde
Aynı kodu 3 ayrı makinede (aynı GPU model — RTX 4090) çalıştırdığında loss curve'lerini bit-exact üst üste binmiş göreceksin. Bunu bir CI testine bağlayabileceksin. Bir takım arkadaşın 'sende çalışıyor bende çalışmıyor' diye geldiğinde gözünü kıpırdatmadan kök nedeni 5 dakika içinde bulacaksın.
1. Niye Bu Kadar Önemli? (Maliyet Hikayesi)#
Tipik ML takımında senin sandığın şey: "fark biraz, önemli değil, model %0.3 değişmiş işte." Gerçek hikaye:
- Bug bisection patlar — git bisect yapacaksın ama her commit farklı loss veriyor, hangisi kötüleşti anlamıyorsun.
- Hyperparameter sweep'ler yalan söyler — lr=2e-5 vs lr=3e-5 arasındaki "fark" aslında seed gürültüsü içinde.
- Eval shopping ortaya çıkar — "10 seed denedik, en iyi olan bu" şeklinde rapor (= reviewer #2'nin avı).
- Paper reproduction imkânsızlaşır — başkasının cookbook'unu denersin, tablosuna yaklaşamazsın, belki senin hatan, belki onun.
Hızlı sayı: tipik 8B LoRA SFT run'ı ~1 saat × 450W × ₺3.5/kWh ≈ ₺1.6. Önemsiz gibi. Ama 30 deneyle bir sweep yapıyorsan ₺50 ve 3 gün. Seed gürültüsünü filtrelemek için 5 seed gerekiyorsa ₺250 ve 2 hafta. Reproducibility yokken bu rakamlar 2-3x'lenir.
2. Seed Hiyerarşisi (Hepsi Aynı Sayı Olmamalı)#
torch.manual_seed(42)| RNG | Etkilediği | Set yöntemi |
|---|---|---|
Python random | data augmentation, shuffle | random.seed(...) |
| NumPy | sklearn, datasets, custom np ops | np.random.seed(...) |
| PyTorch CPU | random init, dropout (CPU yolu) | torch.manual_seed(...) |
| PyTorch CUDA | dropout (GPU), CUDA op'lar | torch.cuda.manual_seed_all(...) |
| DataLoader workers | her worker'ın augmentation seed'i | worker_init_fn |
| Hash | dict ordering, set iteration | PYTHONHASHSEED |
torch.manual_seedcuda.manual_seed_allpython
# === Full deterministic init bloku (cookbook'un her Lab'ında bulunur) ===import os, random, hashlibimport numpy as npimport torch def set_full_seed(seed: int = 42): """Tüm RNG katmanlarını + cuDNN/cuBLAS deterministic'i set eder.""" # Python-level random.seed(seed) os.environ["PYTHONHASHSEED"] = str(seed) # NumPy np.random.seed(seed) # PyTorch torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) # cuBLAS workspace — ":4096:8" deterministic GEMM gerektirir # PyTorch >= 1.11: CUBLAS_WORKSPACE_CONFIG set edilmezse use_deterministic_algorithms hata atar os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" # cuDNN torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False # benchmark seçim non-deterministic # ATen-level deterministic enforcement torch.use_deterministic_algorithms(True, warn_only=True) # warn_only=False yaparsan tek bir non-det op tüm run'ı patlatır — gerçek "strict mode" return seed def seed_worker(worker_id: int): """DataLoader worker'ları için seed_fn — her worker farklı ama deterministic.""" worker_seed = torch.initial_seed() % 2**32 np.random.seed(worker_seed) random.seed(worker_seed) # Kullanımseed = set_full_seed(42)gen = torch.Generator(); gen.manual_seed(seed) # DataLoader'a verilecek from torch.utils.data import DataLoaderloader = DataLoader( dataset, batch_size=8, shuffle=True, num_workers=4, worker_init_fn=seed_worker, generator=gen, # shuffle'ın seed'i persistent_workers=True,)deterministic init bloku — her training script'in ilk hücresi
3. cuDNN'in İçindeki Belalar#
cuDNN, NVIDIA'nın conv/RNN/attention için ürettiği kernel kütüphanesi. Performans için iki "akıllı" davranışı vardır:
- Algorithm selection () — ilk forward'da girdi şekli için en hızlı algoritmayı seçer, sonra kullanır. Sorun: "en hızlı" karar non-deterministic; aynı tensor için iki run'da iki ayrı algorithm seçebilir.
benchmark=True - Non-deterministic kernels — bazı conv backward implementasyonları atomicAdd kullanır; aynı problem 2 kez çözüldüğünde bit-different sonuçlar verir (FP toplama sırası değişir).
Çözüm:
- (algorithm seçimi sabit)
cudnn.benchmark = False - (atomicAdd'lı kernel'leri kullanmaz; gerekirse daha yavaş ama deterministic alternatife düşer)
cudnn.deterministic = True
Maliyeti: İlk forward ~%5-15 yavaşlar (algorithm tuning yapılmadığı için). Çoğu LLM FT'te conv kullanmadığımız için maliyet pratikte sıfır. Vision/Whisper FT'te ~%5-10.
cuBLAS workspace config#
CUBLAS_WORKSPACE_CONFIG=":4096:8":4096:8Eğer set etmezsen PyTorch ≥ 1.11'de şu hata gelir:
RuntimeError: Deterministic behavior was enabled with either `torch.use_deterministic_algorithms(True)`... You can either restart the Python session ... or use the workaround ... For more information, go to https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility
4. ATen Non-Deterministic Op'ları#
PyTorch'un C++ tarafı (ATen) bazı op'ları doğal olarak non-deterministic. Tipik kahramanlar:
| Op | Niye non-det | Cookbook'ta nerede karşılaşır |
|---|---|---|
scatter_add_ | atomicAdd, FP toplama sırası | scatter-based losses |
index_add_ | aynı sebep | embedding gradient update |
grid_sampler_backward | atomicAdd | vision FT |
max_pool3d_backward | atomicAdd | 3D vision |
nn.functional.interpolate | atomicAdd | vision |
torch.nn.functional.embedding_bag | atomicAdd | bazı recsys/NLP |
torch.use_deterministic_algorithms(True)UserWarning: scatter_add_cuda_kernel does not have a deterministic implementation, but you set 'torch.use_deterministic_algorithms(True)'. ...
Strateji:
- ile başla → run sonunda uyarıları topla.
warn_only=True - Eğer kritik op değilse (örn. eval'da scatter), ignore.
- Eğer kritik op ise (örn. train loop'unda embedding update), CPU'ya offload veya deterministic alternatife yaz.
Sertifika almak için cookbook'ta zorunlu: her Lab'ın final run'ında deneyip patlamayan hat sağlamak — yani run baştan sona deterministic.
warn_only=Falsepython
# === Repro CI testi (cookbook için zorunlu) ===# Aynı kodu 2 kez çalıştır, son loss'lar bit-exact olmalı. import subprocess, json, hashlib def run_once(seed: int = 42) -> dict: """Eğitim script'ini bir kez çalıştırır, son loss + bazı tensor hash'leri döner.""" result = subprocess.run( ["python", "train.py", "--seed", str(seed), "--max_steps", "50", "--out", "ci.json"], capture_output=True, text=True, check=True, ) with open("ci.json") as f: return json.load(f) run_a = run_once(seed=42)run_b = run_once(seed=42) assert run_a["final_loss"] == run_b["final_loss"], ( f"Loss not bit-exact: {run_a['final_loss']} vs {run_b['final_loss']}\n" f"Diff: {abs(run_a['final_loss'] - run_b['final_loss']):.2e}") # Tensor hash karşılaştırma — daha sıkıassert run_a["weight_sha256"] == run_b["weight_sha256"], ( "Weights diverged at step 50 — possible non-deterministic op slipped in") print("✅ Repro CI passed: 2 run, bit-exact final state")iki run'ın bit-exact olduğunu doğrulayan CI testi
🐛 Failure Mode Drill #1 — 'Loss curve'lerim üst üste binmiyor'
Senaryo: Aynı kod, aynı seed, ama 50 step sonra loss'lar 3.412 vs 3.418. Bug nereyi sallıyor olabilir? Hipotezler: (a) `torch.compile` aktif ve farklı warmup ile farklı kernel seçildi → kapat ve test et. (b) DataLoader `num_workers > 0` ama `worker_init_fn` yok → her worker farklı augmentation seed'i alıyor. (c) cuDNN `benchmark=True` kaldı (Trainer default'u olabilir) → ilk forward'da algorithm farklı seçildi. (d) `bfloat16` matmul'larında TF32 fallback değişti → `torch.set_float32_matmul_precision('high')` set et. Drill: 4 hipotezi de tek tek devre dışı bırakarak bisection yap. Cevap çoğu zaman (b) — unutulur.
worker_init_fn5. Bench & Eval: Determinism Maliyetini Sayıyla Görmek#
Cookbook'un sözüne güvenmeden ölçüyoruz. Aynı 8B QLoRA Lab'ı 3 modda:
| Config | step/s | Total run time | Final loss bit-exact? |
|---|---|---|---|
Off (deterministic=False, benchmark=True | 2.10 | 24m | ❌ ±1e-3 |
Light (deterministic=False, benchmark=False | 1.95 | 26m | ❌ ±1e-4 |
Strict (use_deterministic_algorithms(True) | 1.83 | 28m | ✅ 0 |
Sonuç: Strict mode'un maliyeti ~%13 wall-clock. Bunu kabul ediyoruz çünkü 'sende çalıştı'yı bitirmenin değeri çok daha yüksek. Production inference'ta determinism'i kapatabilirsin (latency önemli); training'te asla kapatma.
✅ Bu dersin teslimi
- Yukarıdaki +
set_full_seedkalıbını kendi notebook'una kopyala. 2) Küçük bir loop (10 step MLP train) yaz, 2 kez çalıştır, final loss'ların bit-exact olduğunu göster. 3) `worker_init_fn`'i kaldır, tekrar çalıştır — bit-exact bozulduğunu gör. 4) Sonraki ders: 0.3 — Environment Pinning ve CUDA Version Matrix.seed_worker
Frequently Asked Questions
FlashAttention v2 (Tri Dao impl) deterministic değildir — softmax-stat'lerinin tile-by-tile birikiminde sıra non-det. \`flash_attn_func(..., deterministic=True)\` parametresi var (v2.3+); backward'ı yavaşlatır (~%20) ama bit-exact yapar. v3'te "deterministic mode" daha verimli. Cookbook'ta her ilgili Lab'ın hardware tablosunda bu açıkça not edilir.
Yorumlar & Soru-Cevap
(0)Yorum yazmak için giriş yap.
Yorumlar yükleniyor...
Related Content
Part 0 — Engineering Foundations
Welcome to the Fine-Tuning Cookbook: System, Stage Taxonomy, and the Reproducibility Contract
Start LearningPart 0 — Engineering Foundations
Environment Pinning: uv + pyproject.toml, CUDA Version Matrix, and Container Recipes
Start LearningPart 0 — Engineering Foundations