Storage I/O Engineering: The Art of Letting Your Dataset Slow Down Training (and Prevention)
Dataset bottleneck: GPU is 30% idle waiting for disk. NVMe Gen3/Gen4/Gen5 throughput, dataset format choice (parquet vs arrow vs webdataset), HuggingFace datasets caching, num_workers tuning, prefetch_factor, persistent_workers, pinned memory, FSx vs S3 vs local — recipe to run RTX 4090 + 50K Turkish dataset with 0 idle.
Şükrü Yusuf KAYA
31 min read
Advanced🎯 Bu ders
Bir Llama 3.1 8B QLoRA Lab'ı tek 4090'da 2.5 step/s koşmalı — eğer 1.5 step/s alıyorsan büyük olasılıkla dataloader CPU-bound ya da disk-bound. Bu ders o farkı kapatmak için.
1. NVMe Bandwidth Hızlı Bakış#
| Tip | Sequential Read | Random 4K Read | ML için |
|---|---|---|---|
| SATA SSD | 0.55 GB/s | 95K IOPS | NO |
| NVMe Gen3 x4 | 3.5 GB/s | 600K IOPS | OK kıt |
| NVMe Gen4 x4 (cookbook) | 7.4 GB/s | 1M IOPS | ✅ baseline |
| NVMe Gen5 x4 | 12 GB/s | 1.5M IOPS | premium |
Kontrol komutu:
sudo hdparm -Tt /dev/nvme0n1 # Buffered: ~6800 MB/s (NVMe Gen4) # Random 4K sudo fio --filename=/data/test --size=4G --direct=1 --rw=randread --bs=4k --numjobs=4 --runtime=30
2. Dataset Format Karşılaştırma#
| Format | Random access | Streaming | Compression | Cookbook için |
|---|---|---|---|---|
| JSONL (raw) | yavaş (line scan) | iyi | yok (büyük) | spike |
| Parquet | iyi (row group) | iyi | snappy/zstd | büyük tabular |
| Arrow IPC (HF cache) | çok hızlı (mmap) | iyi | yok | HF datasets default ✅ |
| WebDataset (.tar) | sequential only | en iyi | tar + content compress | çok büyük (TB+) |
| MosaicML Streaming (MDS) | random | en iyi | zstd | TB+ + S3 |
Cookbook tercih kuralı:
- < 5 GB dataset → HF (Arrow IPC, mmap'ten random access)
datasets - 5-100 GB → WebDataset (shards) veya MDS
.tar -
100 GB → MDS + S3 (training-time streaming)
HF datasets'in en güzel yanı: ilk yükleme sırasında Arrow IPC dosyalarına convert edip 'e koyar. Sonraki yüklemeler ile sıfır kopya.
load_dataset(...)~/.cache/huggingface/datasetsmmappython
# === Storage I/O bench — DataLoader'ın gerçek throughput'unu ölç ===import time, torchfrom torch.utils.data import DataLoader, IterableDatasetfrom datasets import load_dataset dataset = load_dataset("malhajar/alpaca-gpt4-tr", split="train")# Tokenize ve formatfrom transformers import AutoTokenizertok = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B") def tokenize(ex): return tok(ex["text"], truncation=True, max_length=2048, padding=False) dataset = dataset.map(tokenize, batched=True, num_proc=8)dataset.set_format("torch", columns=["input_ids", "attention_mask"]) # Bench farklı config'lerdef bench(loader, n=200): it = iter(loader) next(it) # warmup t0 = time.perf_counter() n_tokens = 0 for i in range(n): batch = next(it) n_tokens += batch["input_ids"].numel() elapsed = time.perf_counter() - t0 return n_tokens / elapsed # tokens/sec for nw, pf, pin in [(0, 2, False), (2, 2, True), (4, 2, True), (4, 4, True), (8, 4, True)]: loader = DataLoader(dataset, batch_size=8, num_workers=nw, prefetch_factor=pf, pin_memory=pin, persistent_workers=(nw > 0)) tps = bench(loader) print(f"num_workers={nw:2d} prefetch={pf} pin={pin} → {tps/1e6:5.2f} M tokens/s") # Typical RTX 4090 + Ryzen 7950X output:# num_workers=0 prefetch=2 pin=False → 0.42 M tokens/s ← CPU-bound# num_workers=2 prefetch=2 pin=True → 2.10 M tokens/s# num_workers=4 prefetch=2 pin=True → 3.85 M tokens/s# num_workers=4 prefetch=4 pin=True → 4.20 M tokens/s ← sweet spot# num_workers=8 prefetch=4 pin=True → 3.90 M tokens/s ← context switch overheadDataLoader tuning bench — sweet spot bulma
3. Tuning Kuralları (RTX 4090 + Ryzen 7950X reference)#
| Setting | Cookbook default | Niye |
|---|---|---|
num_workers | min(8, cpu_threads // 2) | her worker bir CPU thread'i tutar |
prefetch_factor | 4 | her worker 4 batch buffer |
pin_memory | True | DMA-direct GPU transfer |
persistent_workers | True | epoch arası worker recreate'i atla |
shuffle | True (train) | gradient noise için |
drop_last | True | son batch'in unstable size'ı atla |
num_workerspin_memory=Truetensor.to("cuda", non_blocking=True)4. WebDataset: Büyük Dataset İçin Standart#
50 GB+ dataset varsa cookbook tercih: WebDataset shard'ları.
.tar# Dataset'i shard'la (her shard ~512MB-2GB tar) python -m webdataset.shardify \ --output 'shards/tr-corpus-{000000..000050}.tar' \ --maxcount 10000 --maxsize 1e9 \ raw_data/*.jsonl
Loading:
import webdataset as wds url = "shards/tr-corpus-{000000..000050}.tar" ds = ( wds.WebDataset(url, shardshuffle=100) .shuffle(1000) .decode("torch") .to_tuple("input_ids", "attention_mask") .batched(8) ) loader = wds.WebLoader(ds, num_workers=4, batch_size=None)
Faydaları:
- Sequential tar read → NVMe full throughput (7.4 GB/s Gen4'te)
- Cloud (S3) ile native —
wds.WebDataset("pipe:aws s3 cp s3://...") - Shard-level shuffle yeterli (intra-shard shuffle de yapılır)
Cookbook'un kuralı: > 10K samples ve > 5 GB → WebDataset.
🐛 FMD — 'GPU %25 idle, dataset bottleneck'
`nvtop` ya da `nvidia-smi dmon -s u` ile GPU utilization'ı izle. %85'in altında uzun süre kalıyorsa dataloader sorunu. Hipotezler: (a) num_workers=0 → tek thread tokenize. Çözüm: num_workers=4+. (b) On-the-fly tokenization eğitim sırasında → pre-tokenize batch öncesi. Çözüm: `dataset.map(tokenize, batched=True, num_proc=8)` ile pre-tokenize ve `set_format("torch")` ile mmap-able yap. (c) pin_memory kapalı → her step CPU→GPU transfer blocking. Çözüm: `pin_memory=True` + `to(...non_blocking=True)`. (d) Dataset NFS'te → random read ms gecikme. Çözüm: local NVMe'ye kopyala (50 GB'a kadar). Drill: nvtop'ta GPU idle gözlemle, 4 hipotezi sırayla elimine et.
5. Bench (RTX 4090 + Ryzen 7950X + NVMe Gen4)#
Llama 3.1 8B QLoRA, batch=2, seq=4096, packing on.
| DataLoader config | tokens/s (loader) | step/s (training) | GPU util | Bottleneck |
|---|---|---|---|---|
| nw=0, no pin | 420K | 0.62 | 28% | CPU |
| nw=2, pin | 2.1M | 1.50 | 78% | borderline |
| nw=4, prefetch=4, pin (default) | 4.2M | 1.78 | 96% | GPU ✅ |
| nw=8, prefetch=4, pin | 3.9M | 1.74 | 95% | overhead |
| WebDataset 4 shards | 5.1M | 1.78 | 96% | GPU (eşit) |
Sweet spot: num_workers=4, prefetch=4, pin_memory=True, persistent_workers=True.
✅ Teslim
- Yukarıdaki bench script'ini koş. 2) GPU utilization'ı `nvtop` ile izle — %95+ olmalı. 3) Cookbook default'unu Lab'larında otomatik kullan. 4) Sonraki ders: 1.7 — Profiling Stack: torch.profiler + nsys + ncu.
Frequently Asked Questions
Default: \`~/.cache/huggingface/datasets\` ve \`~/.cache/huggingface/hub\`. Cookbook NVMe Gen4'te tutmayı önerir. Başka diske taşımak için \`HF_HOME=/data/hf-cache\` env var. Önemli: HDD'ye koyma — ilk \`load_dataset\`'te Arrow IPC dosyaları yazılır ve sonra mmap'tan okunur, HDD'de yavaş.
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
Reproducibility Stack: Seeds, cuDNN Flags, and Deterministic CUDA — End the 'Works on My Machine' Problem
Start LearningPart 0 — Engineering Foundations