Programming Language/Python

[Python] Multiprocessing으로 속도 개선해보기

깜태 2021. 2. 1. 17:51
728x90

TL;DR 3줄요약

파이썬은 GIL으로 인해 쓰레드를 사용하는 데 제약이 있다.

Multiprocessing이냐 multi thread냐는 상황에 따라 다르다.

병렬처리에서는 for문 구조를 줄이는게 핵심이다.

 

데이터셋 전처리 작업으로 코딩하는 와중에, 시간이 너무 오래 걸려서 파이썬 병렬처리에 관하여 공부를 많이 했다. 

새롭게 알게 된 점은 파이썬에서는 GIL(Global Interpreter Lock) 이라고 하여,

다른 언어들과 다르게 물리적으로는 1프로세스에 1개의 쓰레드만 사용할 수 있다.

이에 대해선 파이썬을 개발한 귀도 반 로섬이 의도적으로 설계한 것이라고 한다.

 

I’d welcome a set of patches into Py3k only if the performance for a single-threaded program (and for a multi-threaded but I/O-bound program) does not decrease.

단일 thread 프로그램에서의 성능을 저하시키지 않고 GIL의 문제점을 개선할 수 있다면, 나는 그 개선안을 기꺼이 받아들일 것이다.

 

이 말을 그림으로 이해하자면 아래 그림과 같은데, 자세한 설명은 그림 하단의 출처를 참고하면 된다.

 

출처 : https://m.blog.naver.com/alice_k106/221566619995

 

그러면 위의 그림을 보고 드는 이런 생각이 들 수도 있다.

 

병렬처리에는 Thread도 있고, Process도 있는데
저렇게 Thread가 자원을 나눠 쓰는 방식이면 파이썬에서는 무조건 Process쓰는게 좋은거 아니야?

 

그럼 Thread는 언제 쓰는거야?

 

Multi-Process와 Multi-Thread 차이점은 여러 가지가 있지만, 내 생각은 상황에 따라 다른 것 같다.

 

그 기준을 잡자면

 

- 프로세스 간 통신인지, 프로세스 내 통신 

 

- I/O에 대해서 병목현상이 있는 경우

 

- 컴퓨터의 자원 여유 상황

 

(그 이유에 대한 설명은 이전 포스팅에 적어놓았다.)

 

나의 경우에는 MultiProcessing을 이용해서 속도 개선을 시켰다.

초기의 코드는 다음과 같은데, 1개의 dataset인 15000개 파일을 읽어들여 전처리한 뒤 1개의 파일로 저장하는 방식이다

 

def label_parse(path):
    img_exts = ['.jpg', '.png', '.jpeg']
    label_exts = ['.txt', '.xml', '.json']
    dataList = glob.glob(path+'/**')
    imgList = list()
    labelList = list()
    imgList = [x for x in dataList if os.path.splitext(x)[1] in img_exts]
    labelList = [x for x in dataList if os.path.splitext(x)[1] in label_exts]
    id_dict = dict()
    anno_id = 0
    COCO_img_list = list()
    COCO_anno_list = list()
    ### for문을 이용한 순차적 진행 방식
    for idx, imgpath in enumerate(imgList):
    	# 대충 전처리하는 내용
        imgname = os.path.splitext(imgpath)[0]
        imgname_path = '/'.join(imgname.split('/')[:-1])
        imgname = imgname.split('/')[-1].split('_')[0]
        imgname = imgname_path+'/'+ imgname
        # print(imgname)
        if os.path.exists(imgname + '.txt'):
            img_info, anno_info, anno_id = txt_label(imgname, idx, anno_id, category_list)
        elif os.path.exists(imgname + '.json'):
            img_info, anno_info, anno_id = json_label(imgname, idx, anno_id, category_list)
        elif os.path.exists(imgname + '.xml'):
            img_info, anno_info, anno_id = xml_label(imgname, idx, anno_id, category_list)
        else:
            raise("Can't find annotation")
        COCO_img_list.append(img_info)
        COCO_anno_list.extend(anno_info)
    categories = list()
    for idx, category in enumerate(category_list):
        categories.append({'id' : idx +1, 'name': category, 'supercategory': "Tank"})
    COCO_format = dict()
    COCO_format['info'] = {
            }
    COCO_format['images'] = COCO_img_list
    COCO_format['annotations'] = COCO_anno_list
    COCO_format['licenses'] = {}
    COCO_format['categories'] = categories
    return COCO_format

 

아래는 multiprocessing으로 변환한 결과로, 

순차적인 방식에서 병렬처리로 변경할 때 핵심은, for문 구조를 최대한 줄이는 것이다.

나름의 for문을 줄여본 나의 코드는 다음과 같다.

from multiprocessing import Pool, Process, Manager
import multiprocessing
import os, glob, time

def label_directory(path):
    img_exts = ['.jpg', '.png', '.jpeg']
    label_exts = ['.txt', '.xml', '.json']
    dataList = glob.glob(path+'/**')
    imgList = list()
    labelList = list()
    imgList = [x for x in dataList if os.path.splitext(x)[1] in img_exts]
    labelList = [x for x in dataList if os.path.splitext(x)[1] in label_exts]
    
    id_dict = dict()
    anno_id = 0
    COCO_img_list = Manager().list()
    COCO_anno_list = Manager().list()
    pool = Pool(multiprocessing.cpu_count())
    pool.starmap(label_file, [(imgpath, COCO_img_list, COCO_anno_list) for imgpath in imgList])
    pool.close()
    pool.join()

    categories = list()
    for idx, category in enumerate(category_list):
        categories.append({'id' : idx +1, 'name': category, 'supercategory': "None"})
    COCO_format = dict()
    COCO_format['info'] = {}
    COCO_format['images'] = list(COCO_img_list)
    COCO_format['annotations'] = list(COCO_anno_list)
    COCO_format['licenses'] = {}
    COCO_format['categories'] = categories
    return COCO_format

def label_file(imgpath, COCO_img_list, COCO_anno_list):
    imgname = os.path.splitext(imgpath)[0]
    imgname_path = '/'.join(imgname.split('/')[:-1])
    imgname = imgname.split('/')[-1].split('_')[0]
    imgname = imgname_path+'/'+ imgname
    # print(imgname)
    label_exts = ['.txt', '.xml', '.json']
    # print("PID : " + str(os.getpid()) +  "\t path : ", imgpath)
    if os.path.exists(imgname + '.txt'):
        img_info, anno_info = txt_label(imgname)
    elif os.path.exists(imgname + '.json'):
        img_info, anno_info = json_label(imgname)
    elif os.path.exists(imgname + '.xml'):
        img_info, anno_info = xml_label(imgname)
    else:
        raise("Can't find annotation")
    COCO_img_list.append(img_info)
    COCO_anno_list.extend(anno_info)
    


if __name__=="__main__":
	folder_path = '/input/dataset/path'
    # Train 
    start_time = time.time()
	train_path = os.path.join(folder_path, 'train')   
    COCO_train = label_directory(train_path)
    output = open('train.json', 'w')
    output.write(json_dumps(COCO_train))
    output.close()
    # Val
    val_path = os.path.join(folder_path, 'val')
    COCO_val = label_directory(val_path)
    output = open('val.json', 'w')
    output.write(json_dumps(COCO_val))
    output.close()
    print("Time : ", time.time() - start_time)

 

좌 : multiprocess를 사용하지 않았을 때, 우 : multiprocess를 사용헀을 때

htop을 이용하여 cpu 사용량을 확인해본 결과 cpu가 불타오르는 것을 볼 수 있었다.

그리고, 그 실행결과는 데이터가 대략 15000개 기준으로

cpu 코어 20개를 활용하여서 823초 걸리던 과정이 69초로 줄어든 모습을 볼 수 있었다.

 


[참고]

https://dgkim5360.tistory.com/entry/understanding-the-global-interpreter-lock-of-cpython [개발새발로그]

 

zzaebok.github.io/python/python-multiprocessing/

hamait.tistory.com/755

[GIL에 관한 설명]

ssungkang.tistory.com/entry/python-GIL-Global-interpreter-Lock%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

thrillfighter.tistory.com/621

m.blog.naver.com/alice_k106/221566619995

velog.io/@doondoony/Python-GIL

728x90