본문 바로가기
프로그래밍/기타 Python 모듈

[병렬 프로그래밍] Joblib을 이용한 병렬 프로그래밍 with Python

by 부자 꽁냥이 2022. 4. 3.

이 포스팅은 꽁냥이가 병렬 프로그래밍 공부한 내용을 포스팅하는 곳입니다.


이번에 파이썬(Python) 병렬프로그래밍을 공부하면서 아주 좋은 라이브러리를 찾았다. 바로 Joblib이었다. 이게 왜 좋냐 하면 주피터 노트북에서도 사용할 수 있기 때문이다(파이썬(Python)에서 제공하는 multiprocessing 모듈이 있는데 이는 주피터 노트북에서 사용이 안된다).

 

이번 포스팅에서는 먼저 joblib의 기본적인 사용법을 알아본다. 그리고 이를 응용하여 작업을 병렬로 처리할때 속도 차이가 나는지 알아보려고 한다.

 

1. 기본적인 사용법

2. 응용하기

LIST

   1. 기본적인 사용법

먼저 여기서 사용할 모듈을 임포트 한다. multiprocessing은 파이썬(Python) 병렬 작업을 위한 코어 수를 계산하기 위해 필요하다.

 

import multiprocessing as mp

from joblib import Parallel, delayed

 

이제 병렬 처리에 필요한 코어 수를 정한다. 난 최대 2개까지만 허용했다.

 

num_core = min(mp.cpu_count(), 2)
print(num_core)

 

 

출력해보니 2개로 설정되었다.

 

이제 병렬 처리 대상이 되는 함수를 만들어본다. 나는 제곱값을 계산하는 함수(square)를 만들었다.

 

def square(i):
    return i*i

 

이제 병렬 처리를 위한 joblib 기본 사용법을 알아보자.

 

with Parallel(n_jobs=num_core) as parallel:
    results = parallel(delayed(square)(i) for i in range(10))
print(results)

 

line 1

with 구문(Context Manager)를 이용하여 Parallel 클래스 인스턴스를 만들어준다. 이때 병렬 처리를 위한 코어수(n_jobs)를 지정해준다.

 

line 2

Parallel 클래스 인스턴스에 병렬처리 작업을 넣어준다. 이때 delayed를 사용한다는 것과 그 사용형태를 주목하자. 마치 list comprehension 형태를 보는 것 같다.

각 loop를 돌면서 delayed가 각 코어에 작업을 할당해주면 이를 parallel이 받아서 하나로 다시 합쳐주는 것 같다.

 

위 코드를 실행하면 결과가 제대로 나오는 것을 알 수 있다.

 

 

근데 이 작업이 정말 속도 측면에서 효과가 있는지 확인해보고 싶었다. 우선 코어 개수가 1일 때 얼마나 걸리는지 확인해보았다. 이때 반복수를 크게 하여(range 함수에 100000을 넣어서) 시간 차이를 제대로 보고 싶었다.

 

%%time
with Parallel(n_jobs=1) as parallel:
    results = parallel(delayed(square)(i) for i in range(100000))
print(results[:10])

 

 

3.63초가 나왔다. 이번엔 코어 개수를 2개로 해보았다.

 

%%time
with Parallel(n_jobs=2) as parallel:
    results = parallel(delayed(square)(i) for i in range(100000))
print(results[:10])

 

 

오!! 1.49초로 줄어들었다. 대략 2배 정도의 속도 차이가 났다(코어를 2배로 늘렸으니 어찌 보면 당연한가?).

 

이번엔 병렬 처리를 하지 않고 같은 작업을 수행해보았다.

 

%%time
results = [square(i) for i in range(100000)]
print(results[:10])

 

 

?! 12초가 아니라 0.012초(12ms) 이다. 

 

단순히 for loop 작업은 병렬 처리를 안하느니만 못하다. 이는 (비단 파이썬뿐만 아니라) 병렬 처리를 위해 소모되는 디폴트 시간(코어 별로 작업 준비, 작업 할당, 결과 하나로 합침 등)이 있기 때문에 어찌 보면 당연한 결과라고 생각한다.


반응형

그런데 함수에서는 반복문마다 달라지는 인자가 있고 고정시킬 인자가 있을 수 있다. 이 경우에는 아래와 같이 바꿔주면 된다.

 

delayed(함수)(인자1, 인자2, 인자3 , . . . )

 

예제를 살펴보자. 인자가 추가된 square_two라는 함수를 만들었다. 이때 첫번째 인자는 반복문마다 달라지는 인자고, 나머지 두개 인자는 고정된 인자이다. 여기서 tqdm을 이용하여 Progress Bar가 나타나도록 했다.

 

%%time
from tqdm import tqdm

def square_two(i, j, k):
    return (i+j)*(i+j) - k

with Parallel(n_jobs=2) as parallel:
    results = parallel(delayed(square_two)(i, j=2, k=1) for i in tqdm(range(100000)))
print(results[:10])

 

 

이제 기본적인 사용법은 알아보았으니 좀 더 그럴듯한 예제를 이용하여 joblib의 위대함(?)을 알아보자!


   2. 응용하기

응용을 위해 추가적으로 필요한 모듈을 임포트한다.

 

import matplotlib.pyplot as plt
import os

 

여기서는 50개의 폴더에 각각 matplotlib을 이용한 100개의 그래프를 저장하는 프로젝트를 해볼 것이다.

 

먼저 병렬 처리를 하지 않고 for 문을 사용하여 수행해보자. 코드에 대한 설명은 주석으로 대체한다.

 

%%time
result_save_dir = './results'
repeat_num = 50

for i in tqdm(range(repeat_num)):
    result_save_image_dir = os.path.join(result_save_dir, str(i))
    if not os.path.exists(result_save_image_dir): # 이미지 저장 폴더 생성
        os.makedirs(result_save_image_dir)
    for j in range(100):
        fig = plt.figure(figsize=(6,6))
        fig.set_facecolor('white')
        x = np.random.rand(1000)
        y = np.random.rand(1000)
        plt.scatter(x, y)
        image_path = f'{j}.png'
        plt.savefig(os.path.join(result_save_image_dir, image_path)) # 이미지 저장
        plt.close('all') # 모든 figure 지우기.

 

 

6분 21초가 걸렸다. 

 

이번엔 병렬 처리를 이용해보자. 먼저 작업을 위한 함수를 만들어준다. 첫번째 인자는 반복시 계속 바뀌는 인자고 두번째 인자는 항상 고정이다.

 

def work_func(i, result_save_dir):
    result_save_image_dir = os.path.join(result_save_dir, str(i))
    if not os.path.exists(result_save_image_dir):
        os.makedirs(result_save_image_dir)
    for j in range(100):
        fig = plt.figure(figsize=(6,6))
        fig.set_facecolor('white')
        x = np.random.rand(1000)
        y = np.random.rand(1000)
        plt.scatter(x, y)
        image_path = f'{j}.png'
        plt.savefig(os.path.join(result_save_image_dir, image_path))
        plt.close('all')
    return

 

이제 코어를 4개 이용하여 병렬 처리를 수행한다.

 

%%time
result_save_dir = './results_parallel'
with Parallel(n_jobs=4) as parallel:
    parallel(delayed(work_func)(i, result_save_dir) for i in tqdm(range(repeat_num)))

 

 

!! 2분 7초 걸렸다. 4분 넘게 차이가 났다. 속도의 차이가 느껴지는가?!

 

같은 작업으로 얻어진 여러 파일들을 저장할 때 joblib은 정말 유용하고 빠르다. joblib으로 어떤 걸 더할 수 있을지 연구해보아야겠다.

 

파이썬 병렬 프로그래밍 어린이로서 아직 배울게 많다는 것에 감사함을 느낀다.


댓글


맨 위로