pytorch dataloader在num_workers>0时的“内存泄漏”实为多进程下序列化开销、引用计数异常及copy-on-write失效导致的rss持续增长;random_split因subset强引用原始dataset中大list而使各worker复制全部数据;__getitem__返回list会加剧pickle临时对象堆积;pin_memory=true则被nvidia-smi误计入gpu显存。

PyTorch DataLoader 在 num_workers > 0 时出现的“内存泄漏”,绝大多数情况并不是真正的 C/C++ 级别内存泄漏,而是 Python 对象在多进程间反复序列化、引用计数异常、以及 Copy-on-Write(写时复制)机制失效导致的内存持续增长。它表现为:训练越久,系统内存(RSS)越高,且不随 epoch 结束而回落。
为什么 random_split 会让内存翻倍增长?
因为 random_split 返回的是 Subset,它只保存一个索引列表和对原始 Dataset 的强引用。当原始 Dataset 内部维护了上百万条路径或标签(比如用 list 存的 self.image_paths),那么每个 worker 进程在 fork 时都会拿到这个完整对象的副本 —— 即使你只用其中 5% 的数据。
更糟的是:Python 的引用计数在 fork 后会立即增加,触发 Copy-on-Write 失效,导致所有 worker 进程都各自持有完整数据结构的独立内存副本。
- ✅ 正确做法:切分前先用
np.array替换所有大list,再调用random_split - ✅ 更稳妥做法:直接按索引范围构造两个新
Dataset实例,不共享父集 - ❌ 错误模式:
full_dataset = MyDataset(paths); train, val = random_split(full_dataset, [...])——train和val都持有全部paths
为什么 __getitem__ 返回 list 就会泄漏?
PyTorch worker 进程要把 batch 数据通过 multiprocessing.Queue 传给主进程,必须先 pickle。而 Python 的 list 在序列化时会产生大量临时对象,尤其当 list 元素本身是字符串(如文件路径)时,pickle 过程中会反复创建/销毁引用,GC 来不及回收,内存就卡住了。
- ✅ 替换方案:把
self.image_paths = [...]改成self.image_paths = np.array([...], dtype=object)或dtype='U256' - ✅ 加载图像时也优先返回
np.ndarray,而非PIL.Image或list;torch.tensor()接收np.ndarray更轻量 - ⚠️ 注意:
np.array([...], dtype=object)仍存引用,若元素是大字符串,建议统一转为固定长度bytes或struct编码
pin_memory=True 怎么让 CPU 内存被误算成 GPU 显存?
这不是泄漏,但效果一样吓人:nvidia-smi 显示 GPU memory 占用飙升到 6GB+,而 torch.cuda.memory_allocated() 只有几百 MB。这是因为 Linux 驱动把 pinned memory(锁页内存)的分配也计入了 GPU memory 统计池——尤其在小显存卡(如 RTX 3060)上极易触发。
- ✅ 立即生效的缓解:设
prefetch_factor=1(默认是 2),砍掉一半预取 buffer - ✅ 控制总量:降低
num_workers(从 8 → 2 常可降 1.5GB+ pinned memory) - ✅ Linux 下进阶:启动前加
torch.multiprocessing.set_sharing_strategy('file_system'),避免 fork 时复制 pinned pages
真正难排查的点在于:问题只在大数据集 + 多进程 + 长时间训练时才暴露,且症状像“缓慢内存泄漏”,实则是多个子进程各自累积、互不通信的内存占用。最有效的验证方式不是看代码有没有 del,而是用 ps -eo pid,comm,rss,vsz,args | grep python 观察每个 worker 的 RSS 是否随 epoch 单调上升 —— 如果是,那大概率就是 list 或 Subset 引用惹的祸。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











