最近在跑一个图像分类的小项目,用的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也没用
全部回复
共 31 条Windows下把预处理挪到GPU或主进程试试,worker设成0反而可能更快。
确实,Windows下spawn机制真的是个坑,每个worker都会重新import整个模块,你那些albumentations的转换定义、全局变量全得重新初始化一遍,内存和启动开销自然大。我之前也遇到过,后来直接把预处理改成在dataset的__getitem__里用cv2加numpy写死,绕开那些重型库,速度快了不少,你可以试试把albumentations换成torchvision自带的transform看看。
另外num_workers不是越大越好,Windows上尤其明显,你试过2或者3吗?我这边发现超过4反而因为进程调度和IPC瓶颈更卡。还有个偏方,就是把数据先全部读进内存做成tensor存成.pt文件,训练时直接load,虽然占内存但能彻底绕开IO问题,几千张图完全够用。
至于BrokenPipeError,基本就是主进程和worker通信断了,试试在训练循环外面加个if name == 'main'保护,然后dataloader的persistent_workers=True,有时候能缓解。说实话Windows上PyTorch性能就是不如Linux,如果只是个人项目,装个WSL2跑会省心很多,数据读取这块能差出一倍时间,你值得考虑下。
Windows下spawn确实会重复导入和拷贝数据,试试把num_workers设成0,用内存映射或提前把图片转成npy,比调worker数管用。
Windows下DataLoader慢基本是spawn的锅,每个worker都要重新import一遍库和你的dataset代码,开销比Linux的fork大得多。你可以试试把num_workers设成2到4就行,再往上加反而会因为进程调度和内存复制更卡。另外persistent_workers=True和pin_memory=True能省掉每个epoch重建worker的时间,对Windows挺管用的。如果还是不行,干脆把图片预处理好存成npy或者lmdb,训练时直接读,绕开decode和augment的瓶颈。
Windows下spawn确实会重复导入主模块,把预处理放if __name__里能少踩坑,另外试试persistent_workers加prefetch。
Windows下num_workers开太大反而抢IO,我一般设成CPU核心数一半,再把数据全塞进内存里。
Windows下spawn确实每次都要重新导入,试试把num_workers设成0然后手动用后台线程预取吧。
Windows下DataLoader慢确实是个老问题了,spawn机制导致每个worker都要重新import一遍主模块,如果预处理逻辑写在__main__外面或者有全局初始化代码,开销会成倍放大。我之前也踩过这个坑,num_workers调大反而更卡,后来发现瓶颈根本不在worker数量,而是每个batch的数据要经过pickle序列化跨进程传回主进程,图片解码后的numpy数组动辄几MB,IPC开销直接爆炸。可以试试把pin_memory打开配合non_blocking=True,再把persistent_workers设成True避免每个epoch重建worker。另外albumentations本身挺快的,但如果你在Dataset里每次都重新实例化transform pipeline,那开销也不小,最好放到init里复用。还有个偏方是用Intel的i9或者开Windows的WSL2跑,我在WSL下同样的代码epoch时间直接砍半。如果实在不想折腾,可以考虑用NVIDIA DALI或者把数据预处理好存成lmdb,绕过Python层面的IO瓶颈。
Windows下spawn确实坑,每个worker都要重新import主模块,如果你预处理写在__main__外面或者用了全局变量,每个epoch都在重复拷贝,内存和启动开销直接爆炸。我建议把albumentations的transform定义成可序列化的类或者放到单独模块里,再试试persistent_workers=True配合prefetch_factor,能明显减少worker反复启动的损耗。另外检查下是不是杀毒软件在扫你的图片目录,那个在Windows上影响巨大。
Windows下DataLoader慢这事我太有同感了,之前也被坑过好久。你猜的没错,Windows只能spawn不能fork,所以每个worker启动时都要重新import一遍主模块,还要把dataset、transform这些对象pickle过去,worker一多光启动开销就很吓人,几千张图的项目里这个比例特别明显。num_workers调大反而更卡、还报BrokenPipeError,基本就是内存和句柄被吃爆了,加上Windows的共享内存实现本来就拉胯。我的经验是num_workers别超过4,然后一定要把主训练逻辑塞进if name == 'main'里,不然spawn会递归炸掉。另外albumentations确实有拷贝开销,可以试试用cv2读图、把transform尽量放在GPU上做,或者干脆预先把图片转成numpy/lmdb格式缓存起来,避开每epoch的IO和decode。还有个偏方是用persistent_workers=True配合prefetch_factor,减少反复启动worker的成本,但Windows下效果有限。真要省心的话,数据量不大的情况下num_workers=0配上更快的磁盘或者内存缓存,有时候反而比多worker稳。
Windows上DataLoader慢这事儿我去年也踩过坑,确实挺让人抓狂的。你猜的没错,Windows下多进程用的是spawn而不是fork,每个worker都要重新import一遍主模块、重新初始化,如果你的Dataset在__init__里加载了索引或者做了什么重活,那这个开销会被放大好几倍。num_workers开太大反而更卡,是因为进程创建和IPC通信本身的成本在Windows上就高,加上worker之间还要pickle传输数据,图片多的时候序列化开销很吓人。我后来试过几个方向,一个是用LMDB或者webdataset把图片提前打包成二进制格式,读取时就是连续内存访问,比一张张读JPEG快很多。另一个是把albumentations的增强放到GPU上做,用kornia或者torchvision的v2 transform,CPU这边只做最基础的解码。还有个偏方是把num_workers设成2到4之间,配合persistent_workers=True和prefetch_factor调大一点,减少反复创建进程的次数。如果实在受不了,可以试试把整个训练脚本包在if name == 'main'里,这个在Windows下不加的话spawn会直接炸。你那个BrokenPipeError大概率就是worker被系统或内存压力干掉之后主进程还在等,可以看看是不是某个worker的预处理内存占用太高了。