Pytorch 尝试通过强化cpu使用加快训练和推理速度(一)

目标: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程序计算时间:单核多线程>多核多进程。

猜你喜欢

转载自blog.csdn.net/qq_36401512/article/details/113105009