num_workers设过高导致cpu满载、gpu空等,根本原因是dataloader子进程失控:每个子进程默认启用全部cpu核心运行openmp/mkl(如cv2.imread、torch.mm)并产生pickle开销,小数据集io无压力却空转调度;num_workers=0最稳,linux/macos下2~4为甜点区,超4后吞吐不涨而cpu负载线性上升;需配合pin_memory=true与non_blocking=true成对使用,且须用omp_num_threads等环境变量全局限制子进程线程数,而非仅调torch.set_num_threads()。

num_workers 设太高反而让 CPU 跑满、GPU 干等,根本问题不在模型或 CUDA,而在 DataLoader 子进程失控。
为什么 num_workers 越大越慢?
每个 num_workers 子进程默认启用全部 CPU 核心跑 OpenMP/MKL(比如 cv2.imread、torch.mm),还自带 pickle 序列化开销。小数据集上 IO 没压力,CPU 却在空转调度线程,GPU 一直等不到数据。
-
num_workers=0最稳:主进程加载,适合调试、Windows 或数据量小的场景 - Linux/macOS 下
num_workers=2~4是甜点区;再高基本无效,实测超过 4 后吞吐几乎不涨,CPU 负载却线性上升 - 别盲目按 CPU 核数设(比如 16 核就设 12)——这是常见误判源头
pin_memory=True 却没提速?你漏了 non_blocking=True
pin_memory=True 只把主机内存锁住,真正提速靠 GPU 端异步传输。如果没配 .to(device, non_blocking=True),数据仍同步拷贝,CPU 卡在等待,GPU 也拿不到数据。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 必须成对使用:
pin_memory=True+tensor.to(device, non_blocking=True) - 仅 GPU 训练时才开
pin_memory;CPU 训练开它反而多一次内存拷贝 - 若
num_workers=0,pin_memory无效——因为没子进程,无需固定页内存
OpenCV / Albumentations 在后台偷偷吃光 CPU
很多人调了 num_workers 和 torch.set_num_threads(1) 没用,最后发现是 cv2.imread 或 albumentations.Compose 内部调用了 OpenCV 多线程,默认吃满所有核。
-
torch.set_num_threads(1)只管 PyTorch 算子(如torch.mm),对DataLoader子进程完全无效 - 子进程走 multiprocessing,必须靠环境变量控制:启动脚本最开头就设
os.environ["OMP_NUM_THREADS"] = "1"、os.environ["MKL_NUM_THREADS"] = "1" - Windows 上还要加
mp.set_start_method("spawn"),否则 fork 可能死锁 CUDA 上下文
怎么确认瓶颈真正在哪?别猜,看线程归属
真实瓶颈往往藏在预处理链路最深那层——不是你没开多线程,而是多个库各自开了一套,互相打架。
-
top -H看线程归属:区分是 Python 主线程、DataLoader worker、OpenCV 还是 PIL 在占 CPU -
iostat -x 1看磁盘 IO:%util 接近 100%?说明卡在读文件 -
python -m torch.utils.bottleneck train.py --epochs 1输出 DataLoader 耗时占比、是否同步阻塞——这是最直接的“CT 扫描”
实际调参前,先跑一遍 top -H,比盲目改 num_workers 有用得多。很多“CPU 高 GPU 低”的 case,最后发现是 OpenCV 或 PIL 解码在单核上死循环,而不是数据管道本身设计问题。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










