您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦

本文只是记录我优化的心酸历程。无他,唯记录尔。。。。。小伙伴们可围观,可打call,可以私信与我交流。

干货满满,建议收藏,需要用到时常看看。 小伙伴们如有问题及需要,欢迎踊跃留言哦~ ~ ~。

问题背景

现有一个古诗自动生成的训练接口,该接口通过Pytorch来生训练模型(即生成古诗)为了加速使用到了GPU,但是训练完成之后GPU未能释放。故此需要进行优化,即在古诗生成完成之后释放GPU。

该项目是一个通过Flask搭建的web服务,在服务器上为了实现并发采用的是gunicorn来启动应用。通过pythorch来进行古诗训练。项目部署在一个CentOS的服务器上。

系统环境

软件 版本
flask 0.12.2
gunicorn 19.9.0
CentOS 6.6 带有GPU的服务器,不能加机器
pytorch 1.7.0+cpu

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

优化历程

pytorch在训练模型时,需要先加载模型model和数据data,如果有GPU显存的话我们可以将其放到GPU显存中加速,如果没有GPU的话则只能使用CPU了。

由于加载模型以及数据的过程比较慢。所以,我这边将加载过程放在了项目启动时加载

import torch
device = "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 mp
def 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 threading
threading.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 ThreadPool
pool = 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的各类学习资料

脱单秘籍:回复【脱单】

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

全网同名【码农飞哥】。不积跬步,无以至千里,享受分享的快乐

我是码农飞哥,再次感谢您读完本文

记一次性能优化的心酸历程【Flask+Gunicorn+pytorch+多进程+线程池,一顿操作猛如虎】的更多相关文章

  1. 记一次性能优化,限制tcp_timewait数量,快速回收和重用

    前言 这篇文章的主题是记录一次Python程序的性能优化,在优化的过程中遇到的问题,以及如何去解决的.为大家提供一个优化的思路,首先要声明的一点是,我的方式不是唯一的,大家在性能优化之路上遇到的问题都 ...

  2. 也记一次性能优化:LINQ to SQL中Contains方法的优化

    距离上一篇博文更新已经两个月过去了.在此,先表一表这两个月干了些啥: 世界那么大,我也想去看看.四月份的时候,我入职了上海的一家电商公司,职位是.NET高级开发工程师.工作一个月,最大的感受是比以前小 ...

  3. java笔记--使用线程池优化多线程编程

    使用线程池优化多线程编程 认识线程池 在Java中,所有的对象都是需要通过new操作符来创建的,如果创建大量短生命周期的对象,将会使得整个程序的性能非常的低下.这种时候就需要用到了池的技术,比如数据库 ...

  4. Mysql线程池优化笔记

    Mysql线程池优化我是总结了一个站长的3篇文章了,这里我整理到一起来本文章就分为三个优化段了,下面一起来看看.     Mysql线程池系列一(Thread pool FAQ) 首先介绍什么是mys ...

  5. 基于flask+gunicorn+nginx来部署web App

    基于flask+gunicorn&&nginx来部署web App WSGI协议 Web框架致力于如何生成HTML代码,而Web服务器用于处理和响应HTTP请求.Web框架和Web服务 ...

  6. python独角兽 Flask + Gunicorn

    1.构建程序运行所需的虚拟环境 安装Miniconda 创建虚拟环境 添加程序运行依赖包 添加Gunicorn依赖 方式一:最简单的使用 easy_install 安装或者更新 方式二:下载源码安装 ...

  7. Python+Flask+Gunicorn 项目实战(一) 从零开始,写一个Markdown解析器 —— 初体验

    (一)前言 在开始学习之前,你需要确保你对Python, JavaScript, HTML, Markdown语法有非常基础的了解.项目的源码你可以在 https://github.com/zhu-y ...

  8. 线程锁、threading.local(flask源码中用的到)、线程池、生产者消费者模型

    一.线程锁 线程安全,多线程操作时,内部会让所有线程排队处理.如:list/dict/Queue 线程不安全 + 人(锁) => 排队处理 1.RLock/Lock:一次放一个 a.创建10个线 ...

  9. Flask开发系列之Flask+redis实现IP代理池

    Flask开发系列之Flask+redis实现IP代理池 代理池的要求 多站抓取,异步检测:多站抓取:指的是我们需要从各大免费的ip代理网站,把他们公开的一些免费代理抓取下来:一步检测指的是:把这些代 ...

随机推荐

  1. 鸿蒙内核源码分析(忍者ninja篇) | 都忍者了能不快吗 | 百篇博客分析OpenHarmony源码 | v61.02

    百篇博客系列篇.本篇为: v61.xx 鸿蒙内核源码分析(忍者ninja篇) | 都忍者了能不快吗 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙 ...

  2. P7324-[WC2021]表达式求值【dp】

    正题 题目链接:https://www.luogu.com.cn/problem/P7324 题目大意 给一个只包含\(m\)个值的表达式,\(<\)表前后取最小值,\(>\)表前后取最大 ...

  3. P4606-[SDOI2018]战略游戏【圆方树,虚树】

    正题 题目链接:https://www.luogu.com.cn/problem/P4606 题目大意 给出\(n\)个点\(m\)条边的一张图,\(q\)次询问给出一个点集,询问有多少个点割掉后可以 ...

  4. 算法学习->归并排序

    nwpu-2020级算法实验1-problemB Overview 归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的 ...

  5. PC Register简介

    PC Regiter介绍: JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载 ...

  6. 【vue】获取异步加载后的数据

    异步请求的数据,对它做一些处理,需要怎么做呢?? axios 异步请求数据,得到返回的数据, 赋值给变量 info .如果要对 info 的数据做一些处理后再赋值给 hobby ,直接在 axios ...

  7. 【C++ Primer Plus】编程练习答案——第9章

    1 // chapter09_golf.h 2 3 #ifndef LEARN_CPP_CHAPTER09_GOLF_H 4 #define LEARN_CPP_CHAPTER09_GOLF_H 5 ...

  8. DL4J实战之二:鸢尾花分类

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  9. electron-builder进行DEBUG输出的正确方式

    前言 使用Electron进行打包通常会用到electron-builder或者electron-packager两种工具.在使用electron-builder的时候,由于对机制的不熟悉,我们在打包 ...

  10. 关于web项目中的资源跳转

    1.跳转包括两种方式: 转发 forward 重定向 redirect 2.两种方式的代码: AServlet类: //向request范围中存储数据 request.setAttribute(&qu ...