[기술 소개] Python 병렬처리 Framework를 알아보자

(feat. Ray, Multiprocessing) 백지장도 맞들면 낫다.

Dongwon Lee
KLleon
7 min readJul 27, 2022

--

트랜지스터의 수가 증가함에 따라 CPU의 처리 속도가 2년마다 2배씩 향상된다는 ‘무어의 법칙', 이제는 옛말에 불과하게 되었습니다.

나노 공정의 한계와 발열 때문에 CPU 싱글 코어의 처리 속도는 예전과 비교해 발전이 더디어지고 있는데요.
하지만 머신러닝 연구 및 개발자에게는 갈수록 증가하는 데이터와 미디어의 크기를 보다 빠른 속도로 처리해야 하는 숙명이 있습니다.

이번 포스팅에서는 Python의 코드를 직렬 처리했을 때와 병렬 처리했을 때의 차이점과 코드를 비교해 보도록 하겠습니다.

직렬 컴퓨팅 ( Serial computing)

정의

  • 프로세스를 한 스레드에서 순차적으로 처리한다.

장점

  • 간단한 프로세스의 처리 속도가 빠르다.
  • 하나의 Register를 사용하기 때문에 메모리 공유가 쉽다.

단점

  • 복잡한 프로세스의 처리 속도가 느리다.
출처 : Lawrence Livermore National Laboratory / Livermore Computing Center

예제

# Serial Python Codeimport timedef test(): # 메서드 호출당 약 1초    
time.sleep(1)
start = time.time()cnt = 0
while cnt < 100:
test()
cnt+=1
print('time :', time.time()-start) # time : 100.09337711334229

위 코드에서 test 메서드 처리시간은 1초이고, while 문에서 cnt가 증가함에 따라 순차적으로 처리하기 때문에 총 100번의 loop을 돌게 되어 약 100초가 걸리게 됩니다.
test 메서드의 처리 시간이 늘어나면 늘어날수록 반복한 만큼 곱해져, 처리시간이 증가하게 되는 비효율적인 코드입니다.

병렬 컴퓨팅 (Parallel computing)

정의

  • 한 프로세스를 두 개 이상의 스레드에서 처리한다.
  • 한 코드를 여러 프로세스에 띄워 처리한다.

장점

  • 처리가 복잡한 프로세스의 처리 속도가 빠르다.

단점

  • 간단한 프로세스의 처리 속도가 느리다.
  • 병렬 프로세서인 경우 독립된 Register을 사용하기 때문에 IPC(Inter-Process Communication) Cost가 있다.
출처 : Lawrence Livermore National Laboratory / Livermore Computing Center

두 가지의 Python 병렬처리 프레임워크에 대해서도 다뤄보겠습니다.

Ray

특징

  • 공유 메모리에 저장 및 호출할 수 있다.
  • Actor를 통해 프로세스를 여러 개 띄워서 처리한다.
  • 프로세스 간 데이터 공유할 때 Apache Arrow를 사용하여 Zero-Copy 직렬화하고, Plasma를 이용해 In-Memory Object store에 직렬화된 데이터를 빠르게 공유한다.
  • 기존 코드를 크게 변경하지 않아도 된다.

Multiprocessing

특징

  • Processor를 여러 개 띄워서 처리한다.
  • 프로세스 간 메모리를 공유할 수 있다.
  • 프로세스 간 데이터를 공유할 때 객체를 Pickle 형태로 직렬화(serialize) 해서 공유하기 때문에 큰 객체일 경우 통신 Cost가 크다.
  • 기존 코드를 어느 정도 변경해야 한다.

예제

# Python Rayimport time
import ray
import os
ray.init(ignore_reinit_error=True)
@ray.remote
def test(cnt):
#print(os.getpgid())
time.sleep(1)
return cnt
start = time.time()
def process():
cnt = 0
while cnt < 100:
cnt += 1
result = ray.get([test.remote(cnt) for cnt in process()])
print("time : ",time.time() - start) # time : 9.055373668670654

@ray.remote데코레이터로 actor와 task를 만들 수 있습니다.

remote() 메서드는 value를 shared memory에 저장하고 reference(ObjectRef)를 return 합니다.

ray.get(ObjectRef)를 하게 되면 shared memory에 저장되어 있는 value를 return 합니다.

# Python Multiprocessingfrom multiprocessing import Pool
import time
def test(cnt):
# print(os.getpid()) # 매번 pid가 프로세스의 숫자만큼 다르게 찍힘
time.sleep(1)
return cnt
start = time.time()
pool = Pool(processes=cpu_count()) # cpu_count() : 쓰레드 갯수
def process():
cnt = 0
while cnt < 100:
cnt+=1
yield cnt
pool.map(test, process())
print('time :', time.time()-start) # time : 9.05929160118103

Pool 클래스를 사용하여 프로세스 Pool을 만들 수 있습니다.

또한, pool.map() 은 비동기 병렬 처리 결과가 return 되고, 첫 번째 인자로는 func, 두 번째 인자로는 iterable 인자가 와야 합니다.

직렬 컴퓨팅의 예제로 보여드린 Serial 코드에서는 100초가 걸렸지만, 위 코드로는 Python 병렬처리 framework를 사용해 12개의 프로세서로 약 9초(⌈100/12⌉) 만에 처리했습니다.

Running time이 비교적 짧아서 성능 차이가 두드러지지는 않지만, 아래 그림과 같이 Running time이 높으면 높을수록 확연하게 성능 차이가 나는 것을 볼 수 있습니다.

출처: google 검색

정리

Ray는 프로세스 간 통신을 할 때 Zero-Copy serialization를 위해 Apache Arrow를 사용해 오버헤드를 줄입니다. 또한, 이번 포스트에서는 설명하지 않았지만 serialization을 custom 해서 원하는 형태로 shared memory에 저장할 수 있기 때문에 대용량 데이터를 처리하기에 적합합니다.

Python Multiprocessing은 프로세스 간 통신을 할 때 객체의 크기가 클수록, 객체를 pickle 형태로 serialize 했다가 deserialize 하는 cost도 커지므로 대용량 미디어, 딥러닝 모델 등의 용량이 큰 flow를 처리하기에는 적합하지 않습니다.

이 포스트를 읽으신 여러분이 Python과 더 친해졌길 바라며 글을 마칩니다.*^^*

긴 글 읽어주셔서 감사합니다.

--

--