実験日: 2026-08-24 / PyTorch 2.13.0+cu130
CPU: Core i7-14700F(28 logical CPU)/ GPU: RTX 5070 Ti / WSL2
GPU学習が遅いとき、原因がGPUとは限りません。CPUが画像を読み、transformし、batchにまとめる間、GPUが次のデータを待っていることがあります。
GPU使用率だけを見て判断すると、データ供給側の遅さを見落とします。ここが少し厄介です。
FashionMNIST CNNをnum_workers=0, 1, 2, 4, 8, 12, 16で実際に学習し、軽いToTensorだけの場合と、CPU augmentationを加えた場合を比較しました。worker起動を含む最初のepochと、persistent_workers=Trueでprocessを維持した2 epoch目も分けています。
結果は、augmentationありで0 workerの約8,830 images/sに対し、12~16 workerの反復中央値は約62,100~63,800 images/sでした。約7倍です。一方、pin_memoryは各5回の中央値でOFF 1.634秒、ON 1.635秒となり、今回のend-to-end学習では差を確認できませんでした。
worker数の結論
軽いtransform(ToTensorのみ)
- 0 worker:30,268 images/s
- 2 worker:60,881 images/s
- 4 worker:108,782 images/s
- 8 worker:116,828 images/s
- 12 worker:119,045 images/s
- 16 worker:117,262 images/s
- このsweepでは12付近でplateau
CPU augmentationあり
- 0 worker:8,830 images/s
- 2 worker:20,626 images/s
- 4 worker:41,292 images/s
- 8 / 12 / 16 workerは追加で各5回測定
- 反復中央値:8は55,196、12は62,103、16は63,753 images/s
- 12→16の差は約2.7%。12~16をplateau領域と判断
pin_memory、augmentation、4 worker、各5回
- OFF:中央値1.634秒(1.611~1.649秒)
- ON:中央値1.635秒(1.599~1.669秒)
- 範囲が大きく重なり、end-to-end差なし

図の左側は温間epochです。augmentationの8/12/16だけは各5回の中央値、その他は全条件sweepの値です。右側は同じsweepで、最初のepochとpersistent workerを使った2回目を比較しています。
DataLoaderは何を並列化するのか
Dataset.__getitem__()は、indexを受け取って1 sampleを返します。画像fileの読み込み、decode、crop、rotation、Tensor化などは通常ここで実行されます。
num_workers=0では、main processが学習loopとdata preparationの両方を順番に行います。
main: [load/transform] → [GPU training] → [load/transform] → [GPU training]
num_workers>0では、subprocessが次のbatchを準備し、main processがGPU計算を進めます。
worker 0: [prepare batch A] ───── [prepare batch C]
worker 1: [prepare batch B] ───── [prepare batch D]
main/GPU: [train A][train B][train C][train D]
理想的にはdata preparationとGPU computeが重なります。ただしprocess作成、inter-process communication(IPC)、queue、memory copyにもcostがあります。workerを増やせば常に速いわけではありません。
PyTorch公式のPerformance Tuning Guideも、num_workers>0でasynchronous loadingとaugmentationを重ねられる一方、workload、CPU、GPU、data保存場所に応じて調整すべきだと説明しています。
2種類のworkloadを用意した理由
worker数の効果はDatasetが行う仕事で変わります。
軽い条件です。
transform = transforms.ToTensor()
augmentation条件です。
transform = transforms.Compose([
transforms.RandomResizedCrop(28, scale=(0.75, 1.0)),
transforms.RandomRotation(15),
transforms.ToTensor(),
])
FashionMNISTは小さい28×28画像で、fileもlocal SSD上にあります。それでもrotationとcropを60,000枚へ毎epoch適用するとCPU側の比率が増えます。
「DataLoaderは4 workerが最適」と一般化するのではなく、自分のdecode・transform・storageを含むpipelineを測る必要があります。
実験条件
- FashionMNIST学習画像60,000枚
- CNN 105,866パラメータ
- batch 256、Adam、FP32、TF32無効
num_workers:0 / 1 / 2 / 4 / 8 / 12 / 16prefetch_factor=2(workerありの場合)persistent_workers=True(workerありの場合)pin_memory=Trueを基本条件- 同一loaderで2 epoch
- epoch 1:worker process起動とinitial prefetchを含む
- epoch 2:workerを維持した温間
- DataLoader、host→device転送、forward、backward、Adamをすべて計測
- GPU/libraryは事前に合成1 stepでwarm-up
- CUDA計測の前後で
synchronize()
loader = DataLoader(
dataset,
batch_size=256,
shuffle=True,
num_workers=workers,
pin_memory=True,
persistent_workers=workers > 0,
prefetch_factor=2 if workers > 0 else None,
)
persistent_workersとprefetch_factorはworker=0では使えません。実装時は条件分岐が必要です。
軽いtransformでも4 workerまで大きく伸びた
ToTensorだけの温間throughputは、0 workerの30,268から4 workerの108,782 images/sへ約3.59倍になりました。
FashionMNISTは小さくても、Pythonでsampleを1件ずつ取り出し、PIL imageをTensorへ変換し、batchへcollateする処理があります。小型CNNのGPU計算が速いため、CPU側のわずかな仕事が相対的にbottleneckになりました。
8 workerは116,828、12は119,045、16は117,262 images/sです。4→12の増加は約9.4%ですが、12→16では少し低下しました。
したがって軽い条件では4がcost performanceのよい候補、最高throughputを狙うなら8~12付近です。16 processを常時維持する利益はありませんでした。
augmentationありでは並列化の効果がさらに大きい
augmentation条件のsweepです。
| workers | epoch 1秒 | warm秒 | warm images/s |
|---|---|---|---|
| 0 | 6.327 | 6.795 | 8,830 |
| 1 | 5.792 | 5.757 | 10,422 |
| 2 | 2.967 | 2.909 | 20,626 |
| 4 | 1.493 | 1.453 | 41,292 |
| 8 | 0.903 | 0.852 | 70,398 |
| 12 | 0.830 | 0.853 | 70,322 |
| 16 | 1.055 | 0.937 | 64,050 |
単発sweepでは8と12がほぼ同じで、16は低下しました。しかしepochが1秒未満になるとOS schedulerやprocess状態の揺らぎが順位へ影響します。別のsweepでは12が最高になりました。
そこで8、12、16だけを順序ローテーションして各5回追加測定しました。
- 8 worker:55,196 images/s(54,306~55,660)
- 12 worker:62,103 images/s(61,716~62,660)
- 16 worker:63,753 images/s(62,614~63,974)
絶対値はsweepと追加反復で変わりましたが、0~4から大きく改善し、12~16で伸びが小さくなる傾向は共通です。16は12より約2.7%速いだけなので、このPCでは12付近を実用的なplateau開始点と判断しました。
なぜworker=1の最初だけ遅いのか
軽い条件のworker=1は、epoch 1が3.371秒、温間epochが1.910秒でした。0 workerの約1.97秒より初回だけ大幅に遅くなっています。
1 workerではdata preparationをmain processから分離できますが、並列度は増えず、最初に次のcostを払います。
- subprocess作成
- Python moduleとDataset状態の準備
- queueとshared-memory経路の準備
- 最初のbatchのprefetch
persistent_workers=Trueにすると、Datasetを1周した後もworkerを終了しません。2 epoch目以降はprocess起動を繰り返さずに済みます。
ただしworkerを維持するとprocessとDatasetのmemoryも維持されます。epoch間に長時間止まるjobや、巨大なDataset objectではtrade-offになります。
prefetch_factor=2は何を意味するか
workerがあるとき、各workerは先のbatchをqueueへ準備します。prefetch_factor=2なら概念上、workerごとに最大2 batchを先読みします。
16 workerなら最大32 batch分がpipeline上に存在し得ます。batch 256の高解像度画像ならhost memoryやshared memoryを多く使います。
workerを増やしてBus error、shared memory不足、RAM圧迫が起きる場合は、worker数、batch size、prefetch_factorを下げます。throughputだけでなく/dev/shmとRAMも観測対象です。
pin_memoryは速くならなかった
pin_memory=TrueはDataLoaderがpage-locked host memoryへbatchを置く設定です。pinned memoryからGPUへのcopyは、non_blocking=Trueと組み合わせてasynchronousにできます。
images = images.cuda(non_blocking=True)
labels = labels.cuda(non_blocking=True)
PyTorch公式のpin memory / non_blocking guideも、DataLoader側でpinningし、non-blocking transferを使う方法を解説しています。
しかし今回のaugmentation・4 worker・各5回では次の結果でした。
- OFF:1.634秒(1.611~1.649)
- ON:1.635秒(1.599~1.669)
中央値はほぼ同じで範囲も重なります。28×28×1 channel、batch 256は転送量が小さく、CPU transformと学習計算の方が支配的です。また専用CUDA streamで明示的にprefetchしていないため、転送と計算のoverlap余地も限定的です。
これはpin_memoryが無意味という結論ではありません。高解像度・大batchでH2D転送が大きい場合は効果が変わります。pinned memoryはswapできないため、過剰使用はhost RAMへ圧力をかける点にも注意します。
worker数を決める実践手順
- 実際のDatasetとtransformを固定する
- 0、1、2、4、8のように増やす
- 最初のepochと温間epochを分ける
- throughputが大きく伸びなくなる点を探す
- その周辺だけ複数回測る
- CPU使用率、RAM、
/dev/shmも確認する - augmentationやstorageを変えたら再測定する
「論理CPU数と同じworker」にする必要はありません。今回も28 logical CPUに対し、軽負荷は8~12、augmentationは12付近からplateauでした。
再現方法
cd /mnt/c/Users/user/Documents/Codex/2026-08-24/ko/work/ai_lab
/opt/ai-lab/venv/bin/python experiment_dataloader_bottleneck.py \
--data-dir /opt/ai-lab/data \
--output results/dataloader_bottleneck.json \
--figure ../../outputs/dataloader-bottleneck-workers16.png
図だけ再作成する場合です。
/opt/ai-lab/venv/bin/python plot_dataloader_bottleneck.py \
results/dataloader_bottleneck.json \
../../outputs/dataloader-bottleneck-workers16.png
JSONには全worker sweep、pin_memory各5回、高worker各5回のraw値を保存しています。
まとめ
このPCのFashionMNIST CNNでは、DataLoaderの0 workerが明確なbottleneckでした。軽いtransformでも4 workerで約3.59倍、CPU augmentationありでは12~16 workerが0 workerの約7倍に達しました。
一方、workerを増やす効果はplateauになり、process起動、IPC、prefetch memoryのcostもあります。pin_memoryも今回の小画像ではend-to-end差を確認できませんでした。
結論は、設定値を暗記するのではなく、実際のDataset×transform×storage×modelで、初回と温間を分けて測ることです。画像特徴量HOG+SVMとCNNを同じFashionMNISTで比較すれば、古典機械学習とdeep learningの違いも同じ条件で確認できます。
