Situation: You're running the same training twice with the same seed but get different validation metrics. Here's a systematic debugging checklist, steps to force determinism, and CI tests to catch regressions.Checklist to identify nondeterminism- Confirm seed coverage: ensure you're seeding all RNGs (Python, NumPy, framework, CUDA). - Python: random.seed(SEED) - NumPy: np.random.seed(SEED) - PyTorch: torch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)- Data pipeline: - Disable or fix shuffle (DataLoader shuffle=False) to test. - Check any random augmentations — set deterministic transforms or fixed RNG. - Inspect multi-worker DataLoader: worker_init_fn must set per-worker seeds.- Parallelism / threading: - Set environment vars: OMP_NUM_THREADS=1, MKL_NUM_THREADS=1. - For frameworks, disable async prefetching and non-blocking transfers to test.- cuDNN / GPU nondeterminism: - PyTorch: torch.backends.cudnn.deterministic = True and torch.backends.cudnn.benchmark = False. - Note: some ops lack deterministic implementations; check warnings.- Asynchronous behavior: - Ensure loss/metric reductions are properly synchronized (torch.cuda.synchronize()).- Library / environment: - Verify exact versions of frameworks, CUDA, cuDNN, NCCL. - Reproduce inside a pinned Docker image to rule out host differences.- Hidden sources: - Mixed-precision/autocast can introduce nondeterminism — test with FP32. - Third-party ops/kernels may be non-deterministic.How to make training deterministic (practical steps)- Centralized seeding function:python
# python
def set_seed(seed):
import random, os, numpy as np, torch
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
- DataLoader: set shuffle=False to debug; if using workers, pass worker_init_fn that seeds using torch.initial_seed() ^ worker_id.- Disable async GPU transfers and non-deterministic ops; avoid inplace ops that alter order.- Use deterministic ops where available; if unavailable, accept trade-offs (slower or different algorithms) or pin to single-threaded CPU.- Containerize with Docker and pin exact package versions in requirements + CUDA versions.CI tests to detect regressions- Determinism smoke test: run a short training (single epoch) twice with same seed inside CI container; assert metric differences < epsilon.- Data-pipeline seed test: run data iteration twice and assert identical batches (or hashes) for deterministic config.- Op regression test: include unit tests for custom CUDA/third-party ops; compare outputs given same inputs.- Environment drift detection: fail CI if dependency versions differ from baseline (compare pip freeze or conda list).- GPU vs CPU parity test: run a small model on CPU and GPU deterministically and compare outputs to catch device-specific nondeterminism.- Monitor flaky CI: keep historical metric variance logs and alert when variance increases.What to measure/thresholds- Use deterministic hashing (e.g., SHA256) of model weights and a few minibatch outputs to get exact comparisons; for floating metrics, allow tiny tolerance (e.g., 1e-6) but prefer exact matches when possible.Final notes- Full determinism can be expensive or impossible (some ops nondeterministic). Decide acceptable tolerance and document which parts are deterministic. Use CI to enforce guarantees that matter for reproducibility and model auditing.