Skip to content

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
Reproducibility Stack: Seeds, cuDNN Flags ve Deterministic CUDA — 'Sende Niye Çalışıyor Bende Çalışmıyor' Sorununu Bitir
🎯 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)
tek başına yetmez. Modern PyTorch eğitim akışında en az 6 ayrı RNG vardır:
RNGEtkilediğiSet yöntemi
Python
random
data augmentation, shuffle
random.seed(...)
NumPysklearn, datasets, custom np ops
np.random.seed(...)
PyTorch CPUrandom init, dropout (CPU yolu)
torch.manual_seed(...)
PyTorch CUDAdropout (GPU), CUDA op'lar
torch.cuda.manual_seed_all(...)
DataLoader workersher worker'ın augmentation seed'i
worker_init_fn
(aşağıda)
Hashdict ordering, set iteration
PYTHONHASHSEED
env
torch.manual_seed
PyTorch ≥ 1.8'de CUDA seed'ini de set eder — ama
cuda.manual_seed_all
multi-device için açık güvencedir.
python
# === Full deterministic init bloku (cookbook'un her Lab'ında bulunur) ===
import os, random, hashlib
import numpy as np
import 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ım
seed = set_full_seed(42)
gen = torch.Generator(); gen.manual_seed(seed) # DataLoader'a verilecek
 
from torch.utils.data import DataLoader
loader = 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:
  1. Algorithm selection (
    benchmark=True
    )
    — 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.
  2. 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:
  • cudnn.benchmark = False
    (algorithm seçimi sabit)
  • cudnn.deterministic = True
    (atomicAdd'lı kernel'leri kullanmaz; gerekirse daha yavaş ama deterministic alternatife düşer)
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"
ne anlam ifade eder? Deterministic GEMM (matrix multiply) için cuBLAS'a sabit boyutlu workspace ayır. Aksi halde cuBLAS, çalışma alanına göre farklı algoritmalar seçer (= non-deterministic).
:4096:8
= 4MB workspace × 8 stream — RTX 4090 için sağlıklı varsayılan.
Eğ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:
OpNiye non-detCookbook'ta nerede karşılaşır
scatter_add_
(CUDA, fp16/bf16)
atomicAdd, FP toplama sırasıscatter-based losses
index_add_
(CUDA)
aynı sebepembedding gradient update
grid_sampler_backward
atomicAddvision FT
max_pool3d_backward
atomicAdd3D vision
nn.functional.interpolate
(mode='bilinear', backward)
atomicAddvision
torch.nn.functional.embedding_bag
atomicAddbazı recsys/NLP
torch.use_deterministic_algorithms(True)
aktifken bu op'lardan biri çağrılırsa şu hata ya da uyarı:
UserWarning: scatter_add_cuda_kernel does not have a deterministic implementation, but you set 'torch.use_deterministic_algorithms(True)'. ...
Strateji:
  1. warn_only=True
    ile başla → run sonunda uyarıları topla.
  2. Eğer kritik op değilse (örn. eval'da scatter), ignore.
  3. 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
warn_only=False
deneyip patlamayan hat sağlamak — yani run baştan sona deterministic.
python
# === 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) —
worker_init_fn
unutulur.

5. Bench & Eval: Determinism Maliyetini Sayıyla Görmek#

Cookbook'un sözüne güvenmeden ölçüyoruz. Aynı 8B QLoRA Lab'ı 3 modda:
Configstep/sTotal run timeFinal loss bit-exact?
Off (
deterministic=False, benchmark=True
)
2.1024m❌ ±1e-3
Light (
deterministic=False, benchmark=False
)
1.9526m❌ ±1e-4
Strict (
use_deterministic_algorithms(True)
)
1.8328m✅ 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
  1. Yukarıdaki
    set_full_seed
    +
    seed_worker
    kalı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.

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