资讯专栏INFORMATION COLUMN

记一次性能优化的心酸历程【Flask+Gunicorn+pytorch+多进程+线程池,一顿操作猛如

seasonley / 4631人阅读

摘要:本文只是记录我优化的心酸历程。优化的目标就是在训练完成之后将这部分占用的显存释放掉。而用的是使用创建子进程,不被运行时所支持。第三阶段全局线程池释放子进程的方式也不行了。只能回到前面的线程方式了。在项目启动之后就创建一个全局线程池。

您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦
本文只是记录我优化的心酸历程。无他,唯记录尔。。。。。小伙伴们可围观,可打call,可以私信与我交流。
干货满满,建议收藏,需要用到时常看看。 小伙伴们如有问题及需要,欢迎踊跃留言哦~ ~ ~。

问题背景

现有一个古诗自动生成的训练接口,该接口通过Pytorch来生训练模型(即生成古诗)为了加速使用到了GPU,但是训练完成之后GPU未能释放。故此需要进行优化,即在古诗生成完成之后释放GPU。
该项目是一个通过Flask搭建的web服务,在服务器上为了实现并发采用的是gunicorn来启动应用。通过pythorch来进行古诗训练。项目部署在一个CentOS的服务器上。

系统环境

软件版本
flask0.12.2
gunicorn19.9.0
CentOS 6.6带有GPU的服务器,不能加机器
pytorch1.7.0+cpu

因为特殊的原因这里之后一个服务器供使用,故不能考虑加机器的情况。

优化历程

pytorch在训练模型时,需要先加载模型model和数据data,如果有GPU显存的话我们可以将其放到GPU显存中加速,如果没有GPU的话则只能使用CPU了。
由于加载模型以及数据的过程比较慢。所以,我这边将加载过程放在了项目启动时加载

import torchdevice = "cuda" if torch.cuda.is_available() else "cpu"model = GPT2LMHeadModel.from_pretrained(os.path.join(base_path, "model"))model.to(device)model.eval()

这部分耗时大约在6秒左右。cuda表示使用torch的cuda。模型数据加载之后所占的GPU显存大小大约在1370MB。优化的目标就是在训练完成之后将这部分占用的显存释放掉。

小小分析一波

现状是项目启动时就加载模型model和数据data的话,当模型数据在GPU中释放掉之后,下次再进行模型训练的话不就没有模型model和数据data了么?如果要释放GPU的话,就需要考虑如何重新加载GPU。
所以,模型model和数据data不能放在项目启动的时候加载,只能放在调用训练的函数时加载,但是由于加载比较慢,所以只能放在一个异步的子线程或者子进程中运行。
所以,我这边首先将模型数据的加载过程以及训练放在了一个多带带的线程中执行。

第一阶段:直接上torch.cuda.empty_cache()清理。

GPU没释放,那就释放呗。这不是很简单么?百度一波pytorch怎么释放GPU显存。


轻点一下,即找到了答案。那就是在训练完成之后torch.cuda.empty_cache()代码加上之后再运行,发现并没啥卵用!!!!,CV大法第一运用失败
这到底是啥原因呢?我们后面会分析到!!!

第二阶段(创建子进程加载模型并进行训练)

既然子线程加载模型并进行训练不能释放GPU的话,那么我们能不能转变一下思路。创建一个子进程来加载模型数据并进行训练,
当训练完成之后就将这个子进程杀掉,它所占用的资源(主要是GPU显存)不就被释放了么?
这思路看起来没有丝毫的毛病呀。说干就干。

  1. 定义加载模型数据以及训练的方法 training。(代码仅供参考)
def training(queue):    manage.app.app_context().push()    current_app.logger.error("基础加载开始")    with manage.app.app_context():        device = "cuda" if torch.cuda.is_available() else "cpu"        current_app.logger.error("device1111开始啦啦啦")        model.to(device)        current_app.logger.error("device2222")        model.eval()        n_ctx = model.config.n_ctx        current_app.logger.error("基础加载完成")		#训练方法		result_list=start_train(model,n_ctx,device)        current_app.logger.error("完成训练")        #将训练方法返回的结果放入队列中        queue.put(result_list)
  1. 创建子进程执行training方法,然后通过阻塞的方法获取训练结果
from torch import multiprocessing as mpdef sub_process_train():	#定义一个队列获取训练结果    train_queue = mp.Queue()    training_process = mp.Process(target=training, args=(train_queue))    training_process.start()    current_app.logger.error("子进程执行")    # 等训练完成    training_process.join()    current_app.logger.error("执行完成")    #获取训练结果    result_list = train_queue.get()    current_app.logger.error("获取到数据")    if training_process.is_alive():        current_app.logger.error("子进程还存活")        #杀掉子进程        os.kill(training_process.pid, signal.SIGKILL)    current_app.logger.error("杀掉子进程")    return result_list
  1. 因为子进程要阻塞获取执行结果,所以需要定义一个线程去执行sub_process_train方法以保证训练接口可以正常返回。
import threadingthreading.Thread(target=sub_process_train).start()

代码写好了,见证奇迹的时候来了。
首先用python manage.py 启动一下,看下结果,运行结果如下,报了一个错误,从错误的提示来看就是不能在forked的子进程中重复加载CUDA。"Cannot re-initialize CUDA in forked subprocess. " + msg) RuntimeError: Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, you must use the "spawn" start method

这里有问题,就是 forked 是啥,spawn 又是啥?这里就需要了解创建子进程的方式了。
通过torch.multiprocessing.Process(target=training, args=(train_queue)) 创建一个子进程
fork和spawn是构建子进程的不同方式,区别在于
1. fork: 除了必要的启动资源,其余的变量,包,数据等都集成自父进程,也就是共享了父进程的一些内存页,因此启动较快,但是由于大部分都是用的自父进程数据,所有是不安全的子进程。
2. spawn:从头构建一个子进程,父进程的数据拷贝到子进程的空间中,拥有自己的Python解释器,所有需要重新加载一遍父进程的包,因此启动叫慢,但是由于数据都是自己的,安全性比较高。
回到刚刚那个报错上面去。为啥提示要不能重复加载。
这是因为Python3中使用 spawn启动方法才支持在进程之间共享CUDA张量。而用的multiprocessing 是使用 fork 创建子进程,不被 CUDA 运行时所支持。
所以,只有在创建子进程之前加上mp.set_start_method("spawn") 方法。即

def sub_process_train(prefix, length):    try:        mp.set_start_method("spawn")    except RuntimeError:        pass    train_queue = mp.Queue()    training_process = mp.Process(target=training, args=(train_queue))    ##省略其他代码

再次通过 python manage.py 运行项目。运行结果图1和图2所示,可以看出可以正确是使用GPU显存,在训练完成之后也可以释放GPU。


一切看起来都很prefect。 But,But。通过gunicorn启动项目之后,再次调用接口,则出现下面结果。

用gunicorn启动项目子进程竟然未执行,这就很头大了。不加mp.set_start_method(‘spawn’) 方法模型数据不能加载,
加上这个方法子进程不能执行,真的是一个头两个大

第三阶段(全局线程池+释放GPU)

子进程的方式也不行了。只能回到前面的线程方式了。前面创建线程的方式都是直接通过直接new一个新线程的方式,当同时运行的线程数过多的话,则很容易就会出现GPU占满的情况,从而导致应用崩溃。所以,这里采用全局线程池的方式来创建并管理线程,然后当线程执行完成之后释放资源。

  1. 在项目启动之后就创建一个全局线程池。大小是2。保证还有剩余的GPU。
from multiprocessing.pool import ThreadPoolpool = ThreadPool(processes=2)
  1. 通过线程池来执行训练
  pool.apply_async(func=async_produce_poets)
  1. 用线程加载模型和释放GPU
def async_produce_poets():    try:        print("子进程开始" + str(os.getpid())+" "+str(threading.current_thread().ident))        start_time = int(time.time())        manage.app.app_context().push()        device = "cuda" if torch.cuda.is_available() else "cpu"        model = GPT2LMHeadModel.from_pretrained(os.path.join(base_path, "model"))        model.to(device)        model.eval()        n_ctx = model.config.n_ctx        result_list=start_train(model,n_ctx,device)        #将模型model转到cpu        model = model.to("cpu")        #删除模型,也就是删除引用        del model        #在使用其释放GPU。        torch.cuda.empty_cache()        train_seconds = int(time.time() - start_time)        current_app.logger.info("训练总耗时是={0}".format(str(train_seconds)))    except Exception as e:        manage.app.app_context().push()

这一番操作之后,终于达到了理想的效果。

这里因为使用到了gunicorn来启动项目。所以gunicorn 相关的知识必不可少。在CPU受限的系统中采用sync的工作模式比较理想。
详情可以查看gunicorn的简单总结

问题分析,前面第一阶段直接使用torch.cuda.empty_cache() 没能释放GPU就是因为没有删除掉模型model。模型已经加载到了GPU了。

总结

本文从实际项目的优化入手,记录优化方面的方方面面。希望对读者朋友们有所帮助。

参考

multiprocessing fork() vs spawn()

粉丝专属福利

软考资料:实用软考资料

面试题:5G 的Java高频面试题

学习资料:50G的各类学习资料

脱单秘籍:回复【脱单】

并发编程:回复【并发编程】

											?? 验证码 可通过搜索下方 公众号 获取?? 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/123383.html

相关文章

  • CentOS 下使用 Pipenv + Gunicorn + Supervisor 部署 Flask

    摘要:根据中华人民共和国信息产业部第十二次部务会议审议通过的非经营性互联网信息服务备案管理办法精神,在中华人民共和国境内提供非经营性互联网信息服务,应当办理备案。未经备案,不得在中华人民共和国境内从事非经营性互联网信息服务。 当我们开发了一个简单的 Flask 程序,想把项目部署上线,我们可以选择传统的部署方式或者云部署方式把项目部署上线。在本文中,笔者将使用 阿里云轻量应用服务器 安装 C...

    anyway 评论0 收藏0
  • 基于Flask-Angular项目组网架构与部署

    摘要:基于网,分享项目的组网架构和部署。项目组网架构架构说明流项目访问分为两个流,通过分两个端口暴露给外部使用数据流用户访问网站。通过进行配置,使用作为异步队列来存储任务,并将处理结果存储在中。 基于Raindrop网,分享项目的组网架构和部署。 项目组网架构 showImg(https://cloud.githubusercontent.com/assets/7239657/1015704...

    kelvinlee 评论0 收藏0
  • 如何在 virtualenv 环境下搭建 Python Web

    摘要:生产环境下,自带的服务器,无法满足性能要求。配置前面我们已经在系统环境下安装了安装好的二进制文件放在文件夹下,接下来使用来管理。参考文章探针安装部署部署笔记在生产环境上部署使用详解本文系工程师编译整理。 由于字数的限制,其实本篇文章的全标题为 《如何在 virtualenv 环境下 Django + Nginx + Gunicorn+ Supervisor 搭建 Python Web》...

    roland_reed 评论0 收藏0
  • 如何在 virtualenv 环境下搭建 Python Web

    摘要:生产环境下,自带的服务器,无法满足性能要求。配置前面我们已经在系统环境下安装了安装好的二进制文件放在文件夹下,接下来使用来管理。参考文章探针安装部署部署笔记在生产环境上部署使用详解本文系工程师编译整理。 由于字数的限制,其实本篇文章的全标题为 《如何在 virtualenv 环境下 Django + Nginx + Gunicorn+ Supervisor 搭建 Python Web》...

    godiscoder 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<