CPU利用率低主因是数据加载链路串行、线程失控或GIL未释放:num_workers过高引发OpenMP/MKL线程爆炸;cv2/albumentations默认多线程抢占CPU;pin_memory=False导致重复内存拷贝;Python纯循环阻塞GIL。

Python训练模型时CPU利用率跑不满,通常不是因为CPU不够强,而是数据加载链路里存在隐性串行、线程失控或资源争抢——尤其在 DataLoader 配置不当、预处理库多线程打架、或 GIL 未被绕过时,CPU 根本没机会“全核开干”。
num_workers 设太高反而压低 CPU 利用率
很多人以为开越多 worker 越快,结果 num_workers=16 一跑,top 显示 CPU 占用忽高忽低、平均不到 60%。这不是 CPU 懒,是子进程自己在内耗:
- 每个 worker 默认启用全部 OpenMP/MKL 线程(比如 8 核机器上每个 worker 再开 8 线程),实际并发线程数爆炸,调度开销反超收益
- worker 间通过 pickle 序列化传输数据,小 batch 下序列化/反序列化时间 > 实际预处理时间
- Linux 下 fork 启动方式可能复制 CUDA 上下文,引发锁死或静默失败,部分 worker 停摆却不报错
建议:Linux/macOS 用 num_workers=2–4;Windows 或调试阶段直接设 num_workers=0(主进程加载);启动前加 os.environ["OMP_NUM_THREADS"] = "1" 和 os.environ["MKL_NUM_THREADS"] = "1"。
OpenCV / Albumentations 在后台偷偷占满 CPU
你调了 num_workers、关了 MKL,但 top 一看还是 100% 占用?大概率是 cv2.imread 或 albumentations.Compose 自带的多线程在狂奔:
立即学习“Python免费学习笔记(深入)”;
-
cv2默认启用所有物理核心做图像解码(哪怕你只读一张图) -
albumentations底层调用cv2,不显式禁用就会叠加线程 - 这种占用不会出现在 Python 线程列表里,
ps -T -p $PID才能看到一堆cv2子线程
解决方法:在 __getitem__ 开头加 cv2.setNumThreads(0)(0 表示禁用);若用 albumentations,确认其 transform 不含 cv2 调用,否则同样要配 cv2.setNumThreads(0)。
pin_memory=False 导致 CPU 反复拷贝数据
GPU 训练时若 pin_memory=False(默认值),DataLoader 从 worker 拿到数据后,会先拷贝到普通内存,再由主进程触发一次额外拷贝进锁页内存(pinned memory),最后才传给 GPU。这两次拷贝全靠 CPU,且无法并行:
- 拷贝过程阻塞 worker 继续产出,CPU 利用率出现周期性尖峰+空档
- 尤其在小模型、大 batch 场景下,拷贝时间占比飙升,CPU 看似忙,实则低效空转
-
pin_memory=True后,worker 直接分配锁页内存,省掉中间拷贝环节
注意:pin_memory=True 仅对 GPU 训练有效;若你还在用 CPU 训练,开了反而浪费内存。
GIL 未释放导致 Python 层预处理卡死 CPU
如果你在 __getitem__ 里写了纯 Python 循环(比如手写归一化、字符串切分、JSON 解析),这些操作全程持有 GIL,其他 worker 线程只能排队等——哪怕开了 4 个 worker,也只有一个在真干活:
- GIL 不会因为你用了
multiprocessing就消失;每个 worker 是独立进程,但每个进程内仍是单线程执行 Python 字节码 - 真正释放 GIL 的只有调用 C 扩展的操作(如
numpy数组计算、cv2函数、torch.tensor构造) - 所以避免在
__getitem__写for i in range(...): data[i] *= 0.5,换成data *= 0.5(触发 numpy C 实现)
复杂逻辑尽量提前离线处理存成 .pt 或 .npy,把 __getitem__ 压缩成纯 IO + C 扩展调用,才能让每个 worker 的 CPU 核真正饱和。


















