04 多进程编程以利用多核

多进程编程以利用多核

背景

python evaluate.py跑的太慢了,python enhance.py跑的也不快,所以我想把这两个文件改一下,内部写成多进程的模式。

注意

Python的多线程和多进程要区分。Python的多线程(包括threading模块和ThreadPoolExecutor线程池模块)是用户级线程,且其C语言实现时用了GIL(全局解释器锁),导致根本无法利用多核。

所以Python想要利用多核,需要使用多进程(multiprocessing模块或者multiprocessing.pool.Pool进程池模块)

然后有一个点要清楚

就是OS对线程的实现和Python啊C语言啊提供给你的线程不是一个东西,用户线程的话比如c语言库会给你维护一个线程池,里面的线程最终还是得依赖os的线程实现来做。OS一般都是在内核态实现线程,所以确实是可以在多核上并行的。Python的线程无法并行跟不怪OS,是Python自己用了GIL锁的问题。

其实编程语言设置线程的概念也是为了屏蔽底层的os实现,不管底层os是用的用户级线程实现,还是内核级线程实现,编程语言都可以帮你搞定。像刚才说的python,它的multithread就实现得不好。

然后就是怎么修改代码了

我先修改的enhance.py

  1. 首先import

    import torch.multiprocessing as mp#注意这边不能import multiprocessing,而是要import torch.multiprocessing
    #mp是Python一般写多进程用的;torch.mp的用法和mp几乎一样,区别在于torch.mp一般用在需要共享 GPU 和 CUDA 资源时,这里模型推理(即用模型处理输入)是需要用GPU的,所以应该用torch.mp
    
  2. 然后在__main__开头加上mp.set_start_method('spawn')

    不设置这个的话,默认的创建子进程的方式是fork,跑起来会报错的

    1. fork:

      • 该方法通过复制当前进程来创建子进程。子进程会继承父进程的所有资源,包括打开的文件描述符和内存中的数据。
      • 由于直接复制父进程,启动速度快且内存利用高效。
      • 但是,fork方法有一些缺陷,比如在父进程中某些状态(如锁)可能不适合在子进程中使用。另外,对于一些不支持fork方法的资源,可能会导致不可预期的行为。
    2. spawn:

      • 该方法通过启动一个全新的Python解释器进程来创建子进程。在启动时,只有必要的资源会被传递给子进程,其他资源(如打开的文件描述符)不会被继承。(可能会报错:打开文件过多)
      • 这种方式启动的进程是完全独立的,并且没有继承父进程的状态。
      • 虽然spawn启动速度较慢且内存开销较大,但它避免了fork方法的一些问题,尤其是在涉及多线程和C扩展模块时。

      有利有弊吧。

  3. 重新组织代码,把循环执行的部分组织成一个函数,从而能够利用多线程(进程)来执行

    下面是原本的代码:

    def enhance(config: DictConfig) -> None:
        ###此处省略一大堆无须修改的代码
        
        for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
            ###代码块A
        logger.info("Done!")
    if __name__ == "__main__":
        enhance()
    

    下面是修改后的代码:

    def process_scene_listener_pair(idx, scene_listener_pair, scenes, songs, config, gains, listener_dict, enhanced_folder, enhancer, compressor, separation_model, model_sample_rate, device, sources_order, normalise, num_scenes):
        try:
            if config.separator.device!='cpu':
                torch.cuda.set_per_process_memory_fraction(0.1)#limit cuda usage percentage to be 10% per process
          ###代码块A      
            
        finally:
            if config.separator.device!='cpu':
                torch.cuda.empty_cache()
                
    def print_error(value):
        print("error: ", value)
    
    def enhance(config: DictConfig) -> None:
        ###此处省略一大堆无须修改的代码
        pool = mp.Pool(processes=8)
        for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
            pool.apply_async(process_scene_listener_pair, args=(idx, scene_listener_pair, scenes, songs, config, gains, listener_dict, enhanced_folder, enhancer, compressor, separation_model, model_sample_rate, device, sources_order, normalise, num_scenes), error_callback=print_error)
        pool.close()
        pool.join()
    
        logger.info("Done!")
    if __name__ == "__main__":
        mp.set_start_method('spawn')
        enhance()
    

    其实就是把循环执行的部分组织成一个函数,注意把需要的参数都传进去

  4. 非常重要的一点就是设置torch.cuda.set_per_process_memory_fraction(0.1)

    注意:0.1(比例)*8(最大进程数)要小于1,这样可以保证GPU的显存不会用完,不然一跑就报错显存不足

    然后我又试了一下0.06*16,跑了一会就发现会报错显存不足(而且刚开始可以跑的那些的速度并没有开8个进程的速度快

    需要注意:如果设置的内存比例过低,而每个进程实际需要更多内存,可能会导致进程失败。如果设置合理,则只是可能执行得较慢。

    进一步提升的空间,这边只用了1个GPU,而这台机器有4个,要用多个gpu的话得用pytorch的dataparallel

  5. 及时释放不用的显存(和第4点搭配使用)

    torch.cuda.empty_cache()

  6. def print_error(value):
        print("error: ", value)
    

    注意pool.apply_async有个特点,就是报错的进程不会把报错信息打印出来,所以你只会看到结果的数量不太对,但看不到报错。所以要定义这个函数 error_callback=print_error作为参数传给pool.apply_async从而打印报错信息

  7. 哦还要提一下这个apply_asyncd函数和apply函数的区别:

    apply函数虽然在进程池中调用,但实际上是串行执行的

    apply_async 方法才真正利用了多核优势

我然后修改的是evaluate.py

注意:我发现evaluate.py里没有用到模型推理(即用模型处理数据),所以不需要用GPU

直接放修改后的代码了:

import multiprocessing as mp #不需要那个torch了

def evaluate_scene_listener_pair(idx, scene_listener_pair, scenes, songs, config, gains, listener_dict, enhanced_folder, enhancer, results_file,num_scenes):
    ###代码块A
    ###因为没有用GPU,所以首尾不用像上面那样写了

def print_error(value):
    print("error: ", value)
    
def run_calculate_aq(config: DictConfig) -> None:
    ###此处省略一大堆无须修改的代码
    pool=mp.Pool(processes=64)
    for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
        pool.apply_async(evaluate_scene_listener_pair, args=(idx, scene_listener_pair, scenes, songs, config, gains, listener_dict, enhanced_folder, enhancer, results_file,num_scenes), error_callback=print_error)
    pool.close()
    pool.join()
                
    logger.info("Done!")

if __name__ == "__main__":
    run_calculate_aq()#不需要那个spawn了

怎么看cpu有多少个核呢?

命令:lscpu

打印出来的一大堆参数只用看下面的:

CPU(s) 64 #系统中总共有64个逻辑处理器(逻辑核心)
thread per core 2 #每个物理核心支持2个线程
core per socket 16 #每个插槽(物理CPU)有16个物理核心
socket 2 #系统中有2个物理CPU插槽

其实只用看CPU(s)即可,64

剩余的其实告诉你为什么是64:2*16*2=64

补充一点:

运行一个.py文件怎么知道会不会用GPU呢?其实就看有没有用到模型(包括模型的训练和使用),没用到模型肯定是用CPU跑的,用到模型了除非指定了用CPU跑,否则在检测到电脑有GPU时就会默认用GPU跑。

python enhance.py的结果没问题,但是python evaluate.py的结果出现了问题

应该有800个结果,但少了80个左右,少的也很均匀,又等了半小时毫无反应,我就主动Ctrl+C结束了,发现并没有什么报错,所有没跑完的进程都是由最后的Ctrl+C终止的,不知道为啥。

然后我修改了evaluate.py的代码,为了不让已经跑出的分数再跑一遍,我进行了以下修改:

首先,

    if config.evaluate.batch_size == 1:
        results_file = ResultsFile(
            "scores.csv",
            header_columns=scores_headers,
            append_results=True,###这行是我新加的,使再次运行时不是重新创建表格,而是在原有表格后面补
        )

然后,读表格,用列表existing_scene_listener_pair把scene名称和listener名称成对存下来

    existing_scene_listener_pair=[]
    with open("scores.csv", "r", encoding="utf-8", newline="") as csv_file:
        csv_file.readline()#skip header_line
        for line in csv_file:
            words=line.strip().split(',')
            existing_scene_listener_pair.append([words[0],words[2]])

然后在主循环那边判断一下,如果已经有分数了,就不用再算一次了

pool=mp.Pool(processes=64)
    for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
        scene_id, listener_id = scene_listener_pair
        tem=[str(scene_id),str(listener_dict[listener_id].id)]
        if tem in existing_scene_listener_pair:
            continue
        pool.apply_async(evaluate_scene_listener_pair, args=(idx, scene_listener_pair, scenes, songs, config, gains, listener_dict, enhanced_folder, enhancer, results_file,num_scenes), error_callback=print_error)
    pool.close()
    pool.join()

然后我又跑了一次,进程数还是设置的64,发现还是跑不出结果,就是有切换(等十几分钟会显示在做新的任务)但表格的内容并没有增加。然后我改成16就好了,估计是在同时运行64个进程时,系统可能会耗尽内存资源,导致一些进程被操作系统杀死。

(P.S. 有一首歌的歌名里有逗号,导致用逗号分隔时会出问题,最后可能会有8个scene_listener_pair被算2遍,手动删除即可)

结果展示

这里不放出来了。新的模型确实使平均分提高了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值