num_workers最优值通常等于物理cpu核心数,而非逻辑线程数;设过高会因i/o、pcie或共享内存竞争导致性能下降,且需配合__getitem__优化、windows进程保护及共享内存管理。

num_workers设多少才合理?看CPU物理核心数,不是逻辑线程数
直接结论:num_workers 最优值通常等于机器的**物理CPU核心数**,而非超线程后的逻辑核数(如16线程的8核CPU,优先试 num_workers=8)。设太高反而拖慢——worker进程会竞争磁盘I/O、PCIe带宽或共享内存空间,实测超过物理核数后,训练速度可能下降15–20%。
常见错误是凭直觉设 num_workers=8 或 16,尤其在NVMe SSD上。曾有案例:RTX 3090 + 16线程CPU,num_workers=8 时GPU利用率仅30%,调成 4 后升至85%。原因在于过多worker触发了PCIe通道争抢。
- 查物理核心数(Linux/macOS):
nproc --all或lscpu | grep "Core(s) per socket" - Windows用户可用:
python -c "import os; print(os.cpu_count() // 2 if os.name == 'nt' else os.cpu_count())"(粗略估算) - 首次调优建议从
min(4, 物理核心数)起步,再逐步加1测试
__getitem__里别做同步I/O,否则worker全卡在等磁盘
这是比 num_workers 设置更隐蔽的性能杀手。只要 __getitem__ 中出现 Image.open()、np.load()、open().read() 这类同步磁盘读取,所有worker都会串行等待I/O完成,多进程形同虚设。
典型症状:增加 num_workers 后,htop 显示CPU利用率不升反降,iostat -x 1 中 await 值持续 >10ms。
- 正确做法:把文件读取移到
__init__阶段(适合小数据集可全载入内存) - 或改用异步/内存映射方案,例如
cv2.IMREAD_UNCHANGED | cv2.IMREAD_IGNORE_ORIENTATION加速OpenCV读图 - 对大图像,预解码为
.npy或.lmdb格式,规避重复解码开销
Windows下num_workers>0必加if __name__ == '__main__':保护
不加这句,Windows直接报 RuntimeError: DataLoader worker (pid XXX) exited unexpectedly。根本原因是Windows用 spawn 方式启动子进程,会重新执行整个脚本顶层代码;而Linux/macOS用 fork,复用父进程状态。
错误写法(脚本一运行就崩):
import torch from torch.utils.data import DataLoader, TensorDataset <p>dataset = TensorDataset(torch.randn(1000, 3), torch.randint(0, 2, (1000,))) loader = DataLoader(dataset, num_workers=2) # ← 这里就触发子进程启动</p><p>for x, y in loader: pass </p>
正确写法(必须包裹):
import torch from torch.utils.data import DataLoader, TensorDataset <p>def main(): dataset = TensorDataset(torch.randn(1000, 3), torch.randint(0, 2, (1000,))) loader = DataLoader(dataset, num_workers=2) for x, y in loader: pass</p><p>if <strong>name</strong> == '<strong>main</strong>': main() </p>
共享内存不足、内存溢出的三个关键信号
当看到 Bus error、Killed 或 Segmentation fault 伴随 worker pid 报错,基本锁定是资源耗尽,不是代码逻辑问题。
三个最常被忽略的诱因:
-
/dev/shm空间不够:Linux默认只有64MB,num_workers=8+ 大batch易满。临时扩容:sudo mount -o remount,size=2G /dev/shm - 每个worker复制一份Dataset对象:若
__init__里加载了1GB图像列表,num_workers=8就吃掉8GB内存 -
pin_memory=True加剧显存压力:只在GPU训练且数据能放进显存时启用,否则关掉更稳
真正难调的点从来不是参数本身,而是你得同时盯住GPU利用率、CPU各核负载、磁盘await、共享内存用量四块监控面板——少看一个,就容易把I/O瓶颈误判成worker数量不够。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











