num_workers > 0 变慢或卡死的根本原因是 getitem 中存在不可跨进程共享的操作,如未加锁文件句柄、含全局状态的库调用或.cuda();应确保其仅做纯读取与轻量变换,并用 if name == '__main__': 包裹主逻辑。

为什么 num_workers > 0 有时反而变慢甚至卡死?
根本原因不是进程数设多了,而是数据集的 __getitem__ 方法里做了不能跨进程共享的操作——比如打开了未加锁的文件句柄、调用了含全局状态的库(如某些 OpenCV 初始化)、或直接在 __getitem__ 里创建了 PyTorch Tensor 并用了 .cuda()。多进程启动时会 fork 主进程,这些非法状态被复制后导致子进程阻塞或崩溃。
实操建议:
- 确保
__getitem__只做纯读取+轻量变换(如 PIL 打开 +torchvision.transforms),不涉及 GPU、数据库连接、全局随机种子重设 - 用
if __name__ == '__main__':包裹训练主逻辑,防止 Windows/macOS 上 spawn 模式重复导入执行 - 临时调试时加
pin_memory=False, num_workers=0看是否恢复正常,确认是多进程引发的问题
DataLoader 中 pin_memory=True 的真实作用和代价
它把 CPU 上的 Tensor 预先拷贝到“页锁定内存”(pinned memory),让后续 .to('cuda') 能异步、更快地传输到 GPU。但不是所有场景都加速:如果模型前向/反向本身很轻(如小网络+小 batch),数据搬运不再是瓶颈,开启 pin_memory 反而因额外内存拷贝拖慢整体吞吐。
实操建议:
- 仅当 GPU 利用率长期低于 60% 且
nvidia-smi显示PCIe带宽跑满时,才优先考虑pin_memory=True - 必须配合
num_workers > 0才有效;num_workers=0时开启无意义 - 页锁定内存不可交换,开太多(如 batch_size * 4 * num_workers > 总内存 20%)会导致系统 OOM,尤其在多卡训练时要小心
如何判断 num_workers 设多少才合适?
没有固定公式,取决于 I/O 延迟、CPU 核心数、单次 __getitem__ 耗时。盲目设成 os.cpu_count() 很可能过载——特别是当数据在机械硬盘或远程 NFS 上时,过多进程争抢磁盘寻道反而降低吞吐。
实操建议:
- 从
num_workers=2开始,用torch.utils.benchmark.Timer测单个 epoch 时间,逐步加到 4、8,观察时间是否继续下降 - Linux 下运行
htop,看 Python 子进程 CPU 占用是否持续高于 80%;若频繁降到 0%,说明 I/O 在等磁盘,该换 SSD 或优化读取逻辑 - PyTorch 1.12+ 可启用
prefetch_factor=2(默认值),它控制每个 worker 预取 batch 数;对慢存储可设为 3–4,但会增加内存占用
Windows 下 num_workers > 0 报 BrokenPipeError 或直接退出
Windows 不支持 fork,PyTorch 默认用 spawn 启动子进程,要求所有自定义类(尤其是 Dataset)能被 pickle 序列化。常见失败点是 Dataset 里存了 lambda 函数、打开了文件对象、或引用了模块级不可序列化变量(如 cv2.VideoCapture 实例)。
实操建议:
- Dataset 构造函数中避免保存任何非基本类型对象;把文件路径存为
str,在__getitem__里按需打开 - 检查是否用了
torch.multiprocessing.set_start_method('fork')—— Windows 不支持,删掉这行 - 临时方案:用
num_workers=0+torch.compile(PyTorch 2.0+)加速数据变换部分,比硬扛多进程更稳
实际部署时最容易被忽略的是:不同 storage 类型(本地 SSD / NAS / S3)对 num_workers 的敏感度差异极大,同一套参数在开发机跑得飞快,上生产环境可能雪崩。务必在目标存储上单独压测。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











