线程、进程与顺序循环的RNN训练速度差异及优化咨询
问题分析与优化建议
问题背景
在Windows 10系统(12GB显存Nvidia GPU、4物理/8虚拟CPU、16GB内存)上,针对大量不同数据集训练同一RNN模型,每个训练好的模型保存为独立文件。开发三种实现后出现异常:
- 顺序循环版本:2.38次训练/分钟
- ThreadPool版本:1.20次训练/分钟(速度反而更慢)
- ProcessPool版本:未测试出具体速度
系统状态观测:
- CPU平均使用率30%,内存占用40%-80%
- ThreadPool版本GPU显存几乎完全占满(12GB专用显存+8GB共享显存),同时有3-8个线程运行
- 顺序版本GPU专用显存占用50%-100%
疑问:ThreadPool版本GPU满负载但速度更慢是否反常?GPU并行执行不该更快吗?有哪些显著提速的建议?
核心原因拆解
1. Python线程池的GIL限制
Python全局解释器锁(GIL)会限制同一进程内多线程无法真正并行执行CPU密集型代码。虽然GPU训练是异步的,但训练流程中的CPU预处理、数据加载、模型保存等环节会被GIL阻塞,多线程反而增加切换开销,拖慢整体速度。
2. 单GPU的任务竞争与显存过载
单个GPU无法同时并行执行多个CUDA核函数,多线程提交的训练任务本质是在GPU队列中排队执行。ThreadPool版本显存完全占满后,系统会启用共享显存(系统内存模拟),其读写速度比专用显存慢一个数量级,导致训练过程中频繁显存分页,大幅降低效率。
3. 线程间的GPU上下文冲突
同一进程内的多线程共享同一个GPU上下文,训练任务之间的显存分配、模型参数读写会产生竞争,额外增加同步开销,反而不如顺序执行的流水线顺畅。
针对性优化建议
1. 优先使用ProcessPool而非ThreadPool
多进程能绕开GIL限制,每个进程拥有独立的GPU上下文(需注意单GPU显存容量)。修正当前ProcessPool代码的错误:
- 不要把
if __name__ == '__main__':放在循环内部,避免子进程重复初始化 - 避免依赖全局变量,确保
dir_base、conf、device等在子进程中正确初始化
修正后的ProcessPool核心代码示例:
def parallel_master_routine(tit): dir_base = os.getcwd() + '\\' conf = readjson(dir_base + 'Dati_Apprendimento\\' + '_Conf_Learn_torch.txt') device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") master_routine(tit, dir_base, conf, device) def main(): clean_mem() cores = int(os.cpu_count() * 3 / 4) - 2 dir_base = os.getcwd() + '\\' conf = readjson(dir_base + 'Dati_Apprendimento\\' + '_Conf_Learn_torch.txt') while(conf['loop']): for (_,_,whole_tit) in os.walk(dir_base + conf['dirr']): break whole_tit = [ti for ti in whole_tit if '1m' in ti] with cf.ProcessPoolExecutor(max_workers=cores) as executor: executor.map(parallel_master_routine, whole_tit) if __name__ == '__main__': main()
2. 严格控制GPU显存占用
- 每个训练任务结束后,显式清理显存:在
master_routine末尾添加del model, data; torch.cuda.empty_cache() - 限制并发训练任务数:不要按CPU核心数设置进程数,而是根据单任务显存占用计算最大并发数(比如单任务占6GB显存,就设
max_workers=2),避免触发共享显存
3. 异步CPU预处理与GPU训练解耦
利用当前空闲的CPU资源(使用率仅30%),将数据加载、预处理等CPU密集型操作与GPU训练异步执行:
- 使用PyTorch的
DataLoader设置num_workers>0,让数据预处理在后台线程完成 - 提前加载多个数据集到内存,避免训练过程中频繁磁盘IO
4. 检查训练流程的冗余操作
- 确认
master_routine中是否每次训练都重复初始化模型(如果模型结构固定,可以提前初始化后复制参数,减少开销) - 避免重复将数据加载到GPU,确保每个任务的数据集只加载一次
各版本代码整理
ThreadPool版本
clean_mem() cores = int(os.cpu_count()*3 /4) -2 dir_base = os.getcwd()+'\\' conf = readjson(dir_base+'Dati_Apprendimento\\'+'_Conf_Learn_torch.txt') device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") while(conf['loop']): for (_,_,whole_tit) in os.walk(dir_base+conf['dirr']): break whole_tit = [ti for ti in whole_tit if '1m' in ti] with cf.ThreadPoolExecutor(max_workers=cores) as executor: executor.map(parallel_master_routine, whole_tit)
顺序循环版本
clean_mem() dir_base = os.getcwd()+'\\' conf = readjson(dir_base+'Dati_Apprendimento\\'+'_Conf_Learn_torch.txt') device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") while(conf['loop']): for (_,_,whole_tit) in os.walk(dir_base+conf['dirr']): break whole_tit = [ti for ti in whole_tit if '1m' in ti] for tit in whole_tit: master_routine(tit,dir_base,conf,device)
原ProcessPool版本
def parallel_master_routine(tit): master_routine(tit, dir_base, conf, device) #----------------------------------------------------- def main(): clean_mem() cores = int(os.cpu_count()*3 /4) -2 dir_base = os.getcwd()+'\\' conf = readjson(dir_base+'Dati_Apprendimento\\'+'_Conf_Learn_torch.txt') device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") while(conf['loop']): for (_,_,whole_tit) in os.walk(dir_base+conf['dirr']): break whole_tit = [ti for ti in whole_tit if '1m' in ti] if __name__ == '__main__': with cf.ProcessPoolExecutor(max_workers=cores) as executor: executor.map(parallel_master_routine, whole_tit) if __name__ == '__main__': main()
内容的提问来源于stack exchange,提问作者solocazzimiei
相关产品推荐
相关产品推荐

