目标:pytorch不管是训练还是推理时。cpu的使用率基本只有10%左右,尝试更好的利用cpu来加快训练和推理。
一.在搜索资料时很多作者都提到了python的GIL问题,这边先了解下这个机制直接从例子入手。
本机的cpu是i5-4460 4核4线程
先参考涤生手记的清晰讲述,本实验用的是python3.5故修改下代码。( Python 3.2开始使用新的GIL。在新的GIL实现中其他线程请求这个锁的时候,当前线程就会在5ms后被强制释放掉这个锁。)
GIL锁的释放机制:
Python解释器进程内的多线程是合作多任务方式执行。当一个线程遇到I/O任务时,将释放GIL。计算密集型(CPU-bound)的线程在执行大约100次解释器的计步(ticks)时,将释放GIL。计步(ticks)可粗略看作Python虚拟机的指令。计步实际上与时间片长度无关。可以通过sys.setcheckinterval()设置计步长度。
A1.单线程执行同一个程序调用,耗时71.47s
import time
def counter1():
for i in range(300000000):
i = i + 1
print("this is i:", i + 5)
def counter2():
for j in range(300000000):
j = j + 1
print("this is j:", j + 10)
def main():
start_time = time.time()
for x in range(2):
counter2()
counter1()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
this is j: 300000010
this is i: 300000005
this is j: 300000010
this is i: 300000005
Total time: 71.47001194953918
A2.多线程执行同一个程序,耗时72.08s。
from threading import Thread
import time
def counter1():
for i in range(300000000):
i = i + 1
print("this is i:", i + 5)
def counter2():
for j in range(300000000):
j = j + 1
print("this is j:", j + 10)
def main():
start_time = time.time()
for x in range(2):
t1 = Thread(target=counter2)
t2 = Thread(target=counter1)
t1.start()
t2.start()
t2.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
this is i: 300000005
this is j: 300000010
this is i: 300000005
Total time: 72.07586812973022
this is j: 300000010
显然上面两个案例看出同一个程序,在python中 (Cpthon)单线程反而要比多线程执行的快,因为GIL锁的缘故,多线程实际上需要频繁切换进行并发操作,尤其对于多核CPU来说,存在严重的线程颠簸(thrashing)。
B1.同样使用单线程执行同一个程序,注意同样是上面的程序,这里在代码中增加了sleep(0.01)耗时操作。结果这个时候单线程 执行完程序耗时:42.10s.
import time
def counter1():
for i in range(1000):
i = i + 1
time.sleep(0.01)
print("this is i:", i + 5)
def counter2():
for j in range(1000):
j = j + 1
time.sleep(0.01)
print("this is j:", j + 10)
def main():
start_time = time.time()
for x in range(2):
counter2()
counter1()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
this is j: 1010
this is i: 1005
this is j: 1010
this is i: 1005
Total time: 42.09901976585388
B2.同样使用多线程执行同一个程序,注意同样是上面的程序,这类在代码中增加了sleep(0.01)耗时操作。结果这个时候多线程 执行完程序耗时:22.00s。
from threading import Thread
import time
def counter1():
for i in range(1000):
i = i + 1
time.sleep(0.01)
print("this is i:", i + 5)
def counter2():
for j in range(1000):
j = j + 1
time.sleep(0.01)
print("this is j:", j + 10)
def main():
start_time = time.time()
for x in range(2):
t1 = Thread(target=counter1)
t2 = Thread(target=counter2)
t1.start()
t2.start()
t2.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
this is j: 1010
this is i: 1005
this is i: 1005
this is j: 1010
Total time: 22.006017684936523
为什么同样一个程序,增加了sleep耗时操作以后在python中多线程的操作又比单线程执行的更快了呢?这不就和上面的结果矛盾了吗?这其实说到底就是GIL锁的释放机制了。如上:当一个线程遇到I/O任务时,将释放GIL。计算密集型(CPU-bound)的线程在执行大约100次解释器的计步(ticks)时,将释放GIL。所以说我们增加了sleep耗时操作,相当于将计算型的程序变成了耗时等待的I/O程序,这个时候GIL锁遇到I/O任务时,不会继续等待耗时操作,而是立马释放锁,给其他线程去执行,这样的话效率会比单线程高很多(因为单线程需要等待耗时结束才能继续执行)
二.又找到有人说Python中的多线程是假的多线程。这里参考DarrenChan陈驰的关于‘为什么有人说 Python 的多线程是鸡肋呢?’的回答。
在介绍Python中的线程之前,先明确一个问题,Python中的多线程是假的多线程! 为什么这么说,我们先明确一个概念,全局解释器锁(GIL)。
Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。在多线程环境中,Python虚拟机按照以下方式执行。
1.设置GIL。
2.切换到一个线程去执行。
3.运行。
4.把线程设置为睡眠状态。
5.解锁GIL。
6.再次重复以上步骤。
对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。也就是说,I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。
我们都知道,比方我有一个4核的CPU,那么这样一来,在单位时间内每个核只能跑一个线程,然后时间片轮转切换。但是Python不一样,它不管你有几个核,单位时间多个核只能跑一个线程,然后时间片轮转。看起来很不可思议?但是这就是GIL搞的鬼。任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
首先用python多线程:
#coding=utf-8
from multiprocessing import Pool
from threading import Thread
from multiprocessing import Process
def loop():
while True:
pass
if __name__ == '__main__':
for i in range(3):
t = Thread(target=loop)
t.start()
while True:
pass
CPU占比只有30%多.
python多进程:
#coding=utf-8
from multiprocessing import Pool
from threading import Thread
from multiprocessing import Process
def loop():
while True:
pass
if __name__ == '__main__':
for i in range(3):
t = Process(target=loop)
t.start()
while True:
pass
CPU占比直接飚到100%.一对比说明多进程利用了多核。
按照上面的逻辑,我将第一大点中类比写个多进程B3。结果这个时候多线程 执行完程序耗时:21.89s。对比之前的22.00s。有效果但效果很小,应该是sleep时间过长的缘故(完全有时间单核来回切换做双核的事情)。
import time
from multiprocessing import Process
def counter1():
for i in range(1000):
i = i + 1
time.sleep(0.01)
print("this is i:", i + 5)
def counter2():
for j in range(1000):
j = j + 1
time.sleep(0.01)
print("this is j:", j + 10)
def main():
start_time = time.time()
for x in range(2):
t1 = Process(target=counter1)
t2 = Process(target=counter2)
t1.start()
t2.start()
t2.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
this is i: 1005
this is j: 1010
this is j: 1010
this is i: 1005
Total time: 21.886003255844116
按照上面我的逻辑。当我把sleep设置为0.0001时。多线程:4.01s VS 多进程:3.71s 差距不大,但效果比之前明显。 (之后多次尝试,发现0.001-0.000000001之前时间都是4sVS3.7s左右。其中什么有什么瓶颈么?)
我接着修改循环次数: 将B2和B3 中循环改为100,sleep还是0.0001时。多线程:0.40s VS 多进程:1.08s
将B2和B3 中循环改为10000,sleep还是0.0001时。多线程:40.01s VS 多进程:29.94s
最后极端点 将B2和B3 中循环改为5,sleep还是0.1时()。多线程:1.01s VS 多进程:1.82s
第一大点中已经说明sleep耗时操作,相当于将计算型的程序变成了耗时等待的I/O程序。而循环次数相当于计算密集程度。这些对比说明 B2和B3 中有I/O程序和计算密集程序,两者相互影响无法单纯的做对比。
既然上面的例子不合适多线程优势。我再修改成多进程A3。结果这个时候多线程 执行完程序耗时:36.63s。对比之前的72.08s的将近一半。
from threading import Thread
import time
from multiprocessing import Process
def counter1():
for i in range(300000000):
i = i + 1
print("this is i:", i + 5)
def counter2():
for j in range(300000000):
j = j + 1
print("this is j:", j + 10)
def main():
start_time = time.time()
for x in range(2):
t1 = Process(target=counter2)
t2 = Process(target=counter1)
t1.start()
t2.start()
t2.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
this is j: 300000010
this is i: 300000005
this is i: 300000005
this is j: 300000010
Total time: 36.62899208068848
A1,A2,A3 属于计算密集型程序。A2,A3对比发现计算密集型程序计算时间:多核多进程>单核多线程。
再类比B2和B3做个单纯的多线程和多进程I/O程序对比C。
from threading import Thread
import time
from multiprocessing import Process
def counter1():
time.sleep(0.1)
def counter2():
time.sleep(0.1)
def main_Thread():
start_time = time.time()
for x in range(100):
t1 = Thread(target=counter1)
t2 = Thread(target=counter2)
t1.start()
t2.start()
t2.join()
end_time = time.time()
print("Thread Total time: {}".format(end_time - start_time))
def main_Process():
start_time = time.time()
for x in range(100):
t1 = Process(target=counter1)
t2 = Process(target=counter2)
t1.start()
t2.start()
t2.join()
end_time = time.time()
print("Process Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main_Thread()
main_Process()
Thread Total time: 10.126013040542603
Process Total time: 49.22399544715881
C中看出来I/O程序计算时间:单核多线程>多核多进程。