根本原因是linux下fork启动方式会复制主进程的cuda上下文等非fork-safe全局状态,导致子进程初始化阻塞;必须在if name == '__main__':中调用torch.multiprocessing.set_start_method('spawn', force=true),并确保所有cuda操作延后至dataloader创建之后。

卡死不是代码写错了,是子进程启动时复用了主进程的 CUDA 上下文或全局状态,导致 GPU 操作阻塞在初始化阶段。
为什么 fork 启动方式在 Linux 下必然卡死
Linux 默认用 fork 创建子进程,它会完整复制主进程内存镜像——包括已初始化的 torch.cuda 状态、OpenCV 线程池、logging 配置等。这些状态在子进程中不可重用,尤其当主进程提前调用了 torch.cuda.is_available() 或 cv2.imread(),子进程大概率卡在 futex 等待或直接进入不可中断睡眠(D 状态)。
- 现象:CPU 占用低、GPU 显存无变化、
for batch in dataloader:永远不推进、ps aux看到 worker 进程状态为D - 触发条件:只要主进程碰过
CUDA、OpenCV、matplotlib、h5py中任意一个,风险就存在 - 注意:
fork性能好但危险;spawn安全但必须显式启用,PyTorch ≥ 1.9 仍默认fork
如何正确启用 spawn 启动方式
必须在 if __name__ == '__main__': 块内、所有 torch 导入之后、任何 DataLoader 或模型加载之前调用 torch.multiprocessing.set_start_method('spawn', force=True)。顺序错或位置错都会失效。
- 不能放在模块顶层、函数内部、Jupyter cell 里(会报
RuntimeError: context has already been set) -
force=True很关键:避免旧环境或第三方库(如timm)提前触发 multiprocessing 初始化 - Windows/macOS 默认就是
spawn,但显式声明可统一行为,避免跨平台差异
为什么不能在主进程加载 cuda 模型再传给子进程
spawn 方式下,子进程是全新 Python 解释器,无法继承主进程的 CUDA 上下文、设备句柄或张量状态。直接传递 model.to('cuda') 对象会导致子进程尝试复用无效句柄。
- 轻则报错:
CUDA error: invalid device ordinal - 重则静默卡死:
model.forward()阻塞在 GPU kernel launch,无日志、无异常 - 正确做法:主进程用
map_location='cpu'加载权重,每个子进程独立调用.to(device) - 若模型被
nn.DataParallel或torch.compile包装过,必须在子进程中重建,不能序列化传递
容易被忽略的配套动作
只改启动方式还不够。很多卡死是多个因素叠加触发的,比如 pin_memory=True + fork + 提前调用 torch.cuda.empty_cache(),会加速冲突。
- 训练/推理脚本中,所有
CUDA相关操作(torch.device('cuda')、tensor.cuda()、torch.cuda.is_available())必须挪到DataLoader实例化之后 - 调试阶段或 CPU-only 环境,显式设
pin_memory=False,避免子进程预分配 CUDA 上下文 -
Dataset.__init__中禁止传入不可序列化对象(如threading.Lock、打开的文件句柄、lambda) - 避免在
__getitem__中隐式触发logging/matplotlib/h5py—— 它们不是 multiprocessing-safe 的
最常漏掉的是:把 set_start_method 放在 import torch 之前,或者在 __init__.py 里提前 import 了某个触发 CUDA 初始化的模块。这种“看不见的污染”往往让问题难以复现又极难定位。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











