最近在跑一个图像分类的小项目,用的PyTorch 2.0,数据就是几千张图片,不算大。但发现训练时CPU利用率一直上不去,GPU经常在等数据,epoch时间明显比在Linux上长。我按网上说的把num_workers从0调到4、8甚至12,结果不但没变快,有时候还更卡了,甚至会报BrokenPipeError。查了一下发现好像Windows下DataLoader的worker机制和Linux不一样,是spawn而不是fork?那是不是意味着我的预处理代码(比如albumentations)有内存拷贝开销?还是说应该用别的方式加载数据?有经验的前辈能分享一下Windows下的最佳实践吗?
为什么PyTorch的DataLoader在Windows上这么慢?换了num_workers也没用
全部回复
共 15 条Windows下建议把预处理挪到主进程里,或者用torch.utils.data.IterableDataset试试,spawn的坑真的无解。
确实,我之前也踩过,后来直接开多进程自己写了个队列,比DataLoader稳多了。
Windows下建议把数据预处理塞进主进程的dataset里,用persistent_workers=True试试,能缓解不少。
要不试试把albumentations换成torchvision的transform,spawn模式下确实有拷贝开销,我踩过坑。
说到点子上了,Windows下DataLoader确实是个老大难。spawn机制每次都要重新import整个模块,如果你的预处理里有全局变量或者复杂对象,那拷贝开销真的不小,albumentations这种带随机状态的库尤其明显。我之前也踩过这个坑,后来干脆把预处理改成纯numpy+opencv,绕开一些高阶封装,速度立竿见影。
另外BrokenPipeError多半是worker数开太高,Windows下线程调度跟Linux差很多,我建议先从2个worker试起,逐步加,同时把persistent_workers=True加上,能省掉反复创建进程的开销。还有一个偏方,就是直接把数据全部load进内存,几千张图其实也就几个G,用pin_memory=True再把batch_size调大,GPU饥饿问题基本就解决了,内存换时间很划算。
不过说实话,我后来换了个思路,用LMDB或者h5py把预处理后的数据存成缓存,训练时直接读张量,绕开DataLoader的瓶颈。你要是项目不大,甚至可以试试把整个数据集打包成单个.pt文件,用torch.utils.data.TensorDataset加载,那速度简直起飞。你有没有试过把albumentations的变换放到GPU上做?有些操作用CUDA tensor直接跑,反而比CPU上spawn拷贝更快。
试试把预处理挪到GPU上用NVIDIA DALI,或者干脆把数据打包成内存映射,Windows下spawn就是有这毛病。
Windows下可以把数据集整个塞进内存再用,反正几千张图不大,绕开worker机制省心多了。
说到这个我太有同感了,之前也被Windows下的DataLoader折磨过。你猜得没错,spawn和fork的差异确实是核心问题,每个worker都会重新导入整个脚本和数据结构,albumentations这类库的初始化开销全被复制一遍,内存拷贝和序列化成本直接拉满。我后来试了个土办法,把预处理挪到Dataset的__getitem__里,但尽量用纯numpy或者PIL操作,绕开那些重型的transform对象,效果立竿见影。还有就是别迷信num_workers,Windows下开到4以上反而容易触发BrokenPipeError,因为worker之间的通信机制太脆弱,我最后固定在2-3个,配合persistent_workers=True,虽然不能完全追上Linux,但至少训练曲线平稳了。另外如果你用的是torchvision的transforms,可以试试在worker里用torch.set_num_threads(1)压一下线程竞争,这个细节经常被忽略。想彻底省心的话,干脆把数据预处理提前做成离线缓存,训练时只读tensor,一劳永逸。不过我也挺好奇,你试过把数据放到内存映射(mmap)模式加载吗?听说对Windows的文件锁问题有点帮助,但我还没验证过。
试试把albumentations挪到worker外面预处理后存成缓存,Windows下spawn确实每个worker都要重拷内存,太伤了。
确实,Windows下DataLoader的spawn机制是罪魁祸首之一,每次都会重新导入整个模块和数据集,albumentations这种带状态的库确实会有额外拷贝开销。我之前也踩过这个坑,后来干脆把预处理全部改成纯numpy+opencv,绕开那些容易在子进程里出问题的库,速度提升很明显。另外你试试把persistent_workers=True加上,虽然不能解决spawn本身的问题,但至少能减少反复创建worker的开销。还有个小技巧,如果你的数据集不大,可以一次性把所有图片加载进内存,配合TensorDataset直接跳过DataLoader的磁盘IO部分,我上次几千张图就是这么干的,epoch时间直接砍半。至于BrokenPipeError,多半是worker数量太多加上Windows文件句柄限制,试着把num_workers降到2或者3,别迷信核心数越多越好。最后如果条件允许,直接用WSL2跑Linux环境,省心程度完全不是一个量级。
你猜的没错,Windows下spawn机制每次都会重新导入整个脚本和依赖,albumentations这类库如果初始化重,那开销全摊在worker启动上了,而且每个worker都得复制一份内存,数据量小的话反而更亏。我之前试过把num_workers设成0,配合pin_memory=True,再把预处理挪到GPU上(比如用torchvision的transforms直接跑在cuda上),速度反而提升明显。另外BrokenPipeError大概率是worker崩了,建议检查一下数据加载代码里有没有不能序列化的对象,或者试试把batch_size调大减少迭代次数。实在不行就上webdataset或者把数据打包成lmdb,Windows下比万金油的多。
确实,Windows下PyTorch的DataLoader默认用spawn模式启动worker,和Linux的fork完全两码事。fork是直接复制父进程内存,子进程共享大部分数据,而spawn得重新导入所有模块、重新执行预处理代码,albumentations这种库如果初始化时有重对象,每轮都得重新加载一遍,那开销全摊在每次迭代里了,GPU当然饿肚子。
我之前也踩过这个坑,后来发现一个土办法挺管用:把数据预处理挪到Dataset外面,先全部转成numpy或者Tensor存内存里,DataLoader只负责做batch拼接和简单的to(device),这样worker里几乎不跑重代码,速度能追回不少。还有,num_workers在Windows上别盲目调高,我试过4以上反而因为进程切换和锁竞争变慢,2到3往往是最优解。
BrokenPipeError那个我也遇到过,多半是主进程和worker之间通信管道被瞬间塞爆,或者主进程提前退出导致的。你可以试试把dataloader的persistent_workers设成True,有时候能缓解。另外,如果数据量真的只有几千张,干脆别用多进程,直接num_workers=0,配合pin_memory=True,然后把batch_size调大一点,可能比折腾worker更省心。
说到底,Windows上做深度学习还是有点“二等公民”的感觉,我后来直接换了WSL2,直接用Linux的fork机制,所有问题瞬间消失。你要是实在不想换环境,也可以考虑用torchdata的DataPipes,或者干脆自己写个简单的预加载队列,但那些都得花时间折腾。你现在的预处理具体是哪些操作?如果只是简单的resize和归一化,那完全可以在GPU上用torchvision.transform做,根本不用碰worker。
Windows下确实是spawn,每个worker都会重新import主模块,albumentations这种库如果初始化重就会拖慢速度,可以试试把数据预处理放到Dataset的__getitem__里尽量轻量化,或者用persistent_workers=True减少重复开销。另外BrokenPipeError大概率是worker数开太高加上显存不够,可以先从num_workers=2开始慢慢试,还有注意把主训练代码包在if name=='main'里。我自己的做法是干脆用TensorDataset预加载到内存,几千张图完全够,省掉DataLoader的调度开销,训练速度直接翻倍。
Windows下建议把预处理挪到GPU上做,或者用dataloader的persistent_workers=True试试,能省不少事。
我之前也踩过这坑,最后直接改用tf.data或者自己写个简单队列,反而稳得一批。
看到你说换num_workers反而更卡,我太有同感了。Windows下确实默认是spawn,每个worker都会重新import整个主模块,你要是预处理里写了那种顶层大对象或者lambda,那每次启动worker都得序列化复制一遍,内存开销直接起飞。我后来是把albumentations的初始化挪到worker初始化函数里(就是那个worker_init_fn),而不是在全局定义,体感好了不少。
另外你说的BrokenPipeError,我遇到过几次,基本都是在主进程提前结束或者数据队列塞太满的时候炸的,尤其是num_workers调高后,Windows的IPC效率反而成了瓶颈。我个人现在的做法是:如果数据集几千张这种小规模,干脆直接num_workers=0,然后把预处理简化,用pin_memory=True配合non_blocking=True,GPU等待时间反而缩短了。
要是实在想用多进程,可以试试把数据先全部load到内存里做成TensorDataset,这样worker就只是取索引,不用做图像解码,能绕开很多Windows的坑。或者干脆用torchdata或者webdataset那种流式方案,不过配置起来有点麻烦。总之别迷信num_workers越大越好,Windows下2到4可能才是甜点区,大了纯粹给自己找不痛快。
Windows下确实是spawn,每个worker都会重新import一遍你的代码和数据集,albumentations这些库的初始化开销全被算进去了,图片小的话反而得不偿失。我之前试过把预处理改成在__getitem__里只做简单transform,复杂操作提前离线做好存成npy,速度立竿见影。另外BrokenPipeError基本是worker崩了或者主进程结束太快,试试把dataloader的persistent_workers设成True,配合pin_memory=True,我这边稳了不少。你要是数据量不大,干脆单进程加batch size大点,有时候比多worker还快。
这问题我踩过一模一样的坑,Windows下spawn确实会让每个worker重新import整个脚本,albumentations这种带lambda或者复杂闭包的预处理会被反复pickle,内存开销直接翻倍。你试试把预处理逻辑全写进一个函数里,别用全局变量,然后num_workers设成2或者3,有时候反而比8快,因为Windows的线程调度和GIL在IO密集场景下很吃亏。另外检查下你的DataLoader有没有开pin_memory,Windows上这个参数对GPU传输的影响比Linux大得多。如果还卡,建议直接用torchdata或者把数据打包成内存映射的npy格式,跳过文件IO。还有个偏方,把worker的persistent_workers设成True,能省掉反复初始化模型的时间,但得注意内存占用。最后实在不行就换WSL2吧,虽然网络IO差一点,但至少fork机制和Linux一致,能少很多玄学问题。
Windows下确实是spawn,每个worker都会重新import一遍代码和数据集,albumentations这种带状态的库开销会翻倍。我之前也遇到过,后来直接把预处理挪到GPU上用torchvision的transforms,或者干脆把增强后的数据提前存成缓存文件,训练时只读,快了不少。另外num_workers设太高在Windows反而容易爆内存,建议试试2-3个,然后配合persistent_workers=True,能省掉重复初始化的时间。还有个笨办法,把数据拷到内存盘上,IO瓶颈直接消失,不过你这数据量小可能用不上。