본문 바로가기
Development/Python

Python Multiprocessing 가이드

by IMCOMKING 2020. 2. 20.

Multiprocessing 가이드

공식 레퍼런스 문서를 참고하여 작성하였다. 다음이 요소들이 multiprocessing의 가장 기본이고, 우선 이 네가지만 잘 알면된다.


Process

Pool

Queue

Pipe


Process

단일 프로세스를 생성하는 경우, Process()를 사용한다.

from multiprocessing import Process, Queue

queue = Queue()

p = Process(target = my_function) #, args=(queue, 1))

p.start()

# p.join() # this blocks until the process terminates

# result = queue.get()

https://stackoverflow.com/questions/2046603/is-it-possible-to-run-function-in-a-subprocess-without-threading-or-writing-a-se


따라서 python에서는 일반적으로 thread 대신 multiprocessing을 사용한다

그런데 multiprocessing을 사용하게 되면 shared memory를 사용하기 힘들다는 단점이 존재한다. 


그래서 이를 해결하기 위해 pickle/unpickle 등의 트릭을 이용해서 SHM을 사용하기도 하는데, 이는 공유할 데이터가 커지면 속도가 매우 느려지는 단점이 존재한다.

그대신 python에서 제공하는 sharedctypes를 사용하거나, multiprocessing.Value 나 Array를 사용하는 방법이 존재한다.


http://thousandfold.net/cz/2014/05/01/sharing-numpy-arrays-between-processes-using-multiprocessing-and-ctypes/


https://stackoverflow.com/questions/10721915/shared-memory-objects-in-multiprocessing/10724332


또는 각 process를 생성할 때, args로 공유할 데이터를 전달하는 방식도 존재한다. (이 방법이 가장 손쉬운 방법. 대신 공유할 메모리가 크면 문제가 생김)




multiprocess의 시작 방법


새로운 process가 시작되는 방법은 세가지가 있는데, forkserver라는 세번 째 방법은 minor한 방법이므로, 여기서는 spawn과 fork만 알아본다.


spawn: process생성 속도는 fork보다 살짝 느리다. child process는 최소한의 자원만 승계받는다. Unix와 Windows에서 모두 사용가능하다. Windows에서 multiprocess사용시 기본옵션이다.


fork: child process는 parent의 모든 자원을 매우 효과적으로 승계받는다. Unix에서 multiprocess사용시 기본 옵션이다.


시작 방법은


mp.set_start_method('spawn') 또는 

mp.set_start_method('fork') 를 이용해서 바꿀 수 있는데, 프로세스가 한 번 생성되고 난 다음에는 설정을 바꿀 수 없다. 


그러나 예외적으로 그런 기능이 필요한 경우가 있는데, 예를 들면 library내에서 mp 사용 시, user의 mp.context와 충돌을 방지해야하므로 multi context를 지원해야한다. 이런 경우에는 mp.get_context()를 이용할 수 있지만, 서로 다른 context의 프로세스 끼리는 서로 호환이 되지 않는다.



multiprocess의 통신 방법

Queue: 단방향으로만 정보를 전송한다. 어떤 데이터 type이든지 다 보낼 수 있다.

Pipe: 양방향 queue라고 이해할 수 있다.



API

process.daemon = True로 설정할 경우, parent process가 종료될 때, child process도 같이 종료된다. 


반대로 daemon = False인 경우, parent process가 종료되어도 자동으로 child process가 종료되지 않는다. 이때 parent process는 자신의 process가 마지막으로 exit되기 전에, child process에 join을 하면서 child process의 종료를 기다린다. 


또한 daemon으로 생성된 child process는 스스로 child process(증손자)를 가질 수 없다. 이는 parent가 종료될 때 자동으로 daemonic child process가 종료되면서, 이녀석이 생성한 증손자 process가 orphaned process로 되는 것을 방지하기 위함이다.


process.join() 을 실행하면, 현재 process가 해당 process의 종료를 blocking하면서 waiting한다.



Notebook에서 사용시

spawn을 사용할 경우, if __name__ == '__main__' 으로 보호해주지 않으면 모든 notebook내의 코드를 실행시켜버리는듯하다. 따라서 notebook말고 py를 따로 만들어줘야할듯



Pool

아래의 코드의 시간은 얼마나 걸릴까? 정답은 3.x초이다

왜냐하면 Pool의 크기가 2밖에 되지 않는데, task는 5개이기 때문이다.



%%time

from multiprocessing import Pool


def f(x):

    time.sleep(1)

    return x*x


with Pool(2) as p:

    print(p.map(f, [1, 2, 3, 4, 5]))




Python process ID

os.getpid() 와 os.getppid()를 이용해서 process id를 가져올 수 있다.


Pytorch Multiprocessing


import multiprocessing as mp 대신에
import torch.multiprocessing as mp 을 사용한다.

하나의 네트워크를 두개의 process에서 사용/업데이트 해야하는 경우가 torch.multiporcessing의 대표적인 예시라고 할 수 있다.

Queue를 통해 tensor를 다른 process로 보낼 경우, 이 데이터들은 전부 shared memory로 옮겨지고, 그에 대한 handle을 서로 공유하게 된다. 이는 Gradient또한 동일하다. 



API 문서

와.. pytorch multiproccesing queue 검색하면 나오는글이 거의없음.

오직 spawn만 가능하다.

CUDA는 start method로 오직 spawn또는 forkserver만 제공한다. 

multiprocessing.set_start_method("spawn")


그 이유는 multi-threaded 프로그램을 fork하는 순간 child process가 무조건 죽어버리는 현재의 OS 디자인의 문제라고 한다.

https://github.com/pytorch/pytorch/issues/13883#issuecomment-440004307


RuntimeError: context has already been set

mp.set_start_method("spawn")를 분명히 if __name__ == '__main__': 한 번만 실행했는데, 위와 같은 에러가 발생하는 경우가 있다.


일단 해결방법은 다음의 선택지들이 있다.

1) ctx = mp.get_context("spawn") 을 사용하거나,

2) set_start_method('spawn', True)

https://github.com/pytorch/pytorch/issues/3492


그러나 위의 해결방법은 표면적인 에러는 없앨 수 있지만, 제대로된 multiprocessing 구현을 막는 원인은 해결하지 못한다. 이러한 에러가 뜨는 이유는 근본적으로 내가 import 한 library에서, 혹은 set_start_method()가 실행되기 전에 어떠한 전역변수 생성과정 등에서 이미 set_start_method()가 실행되었다는 의미이다. 이 경우, 위의 해결방법으로 강제로 method를 설정하더라도, 내가 생성한 child process와 parent process간에 method가 달라서, 서로 통신이 불가능하여 구현이 안된다.


서로 다른 method로 생성된 경우, mp.Queue를 이용해서 get을 호출하는 순간 child process가 종료되어 디버깅조차 힘들다. 따라서 위의 에러를 절대 대충 넘기지 않고 제대로 해결하고 넘어가야 할 것이다.



Trouble Shooting

Process가 이상하게 실행되는 경우

process가 정의된 파일을 entry로 사용해야만 한다.

이는 python main.py에서는 if __name__ == '__main__': 으로 보호가 되어있더라도 실제로 process가 시작되는 file이 따로 있는 경우 이런 문제가 생기는 것으로 보인다.

즉 entry file에 process가 정의되어있지 않으면 이런 문제가 생기는 것으로 보인다.



SimpleQueue의 사용

There are a lot of things that can go wrong when a new process is spawned, with the most common cause of deadlocks being background threads. If there’s any thread that holds a lock or imports a module, and fork is called, it’s very likely that the subprocess will be in a corrupted state and will deadlock or fail in a different way. Note that even if you don’t, Python built in libraries do - no need to look further than multiprocessing. multiprocessing.Queue is actually a very complex class, that spawns multiple threads used to serialize, send and receive objects, and they can cause aforementioned problems too. If you find yourself in such situation try using a multiprocessing.queues.SimpleQueue, that doesn’t use any additional threads.







Mutex와  Semaphore의 차이

Mutex는 한 번에 오직 단 하나의 프로세스만 공유자원에 접근할 수 있다.
Semaphore는 동시에 미리 허락된 N개의 프로세스만 공유자원에 접근할 수 있다. Binary semaphore는 mutex와 개념적으로는 비슷하지만 실제 구현상에서는 다소 차이가 존재한다.



Spawn과 fork의 차이

spawn
The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process objects run() method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver.


fork

The parent process uses os.fork() to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic.


forkserver

When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to use os.fork(). No unnecessary resources are inherited.



spawn은 좀 더 최소한의 코드만 담아서 실행하는 개념에 가깝다면, fork는 완전한 자기복제의 의미가 가깝다.


A good rule of thumb is one to two node processes per core, perhaps more for machines with a good ram clock/cpu clock ratio, or for node processes heavy on I/O and light on CPU work, to minimize the down time the event loop is waiting for new events. However, the latter suggestion is a micro-optimization, and would need careful benchmarking to ensure your situation suits the need for many processes/core. You can actually decrease performance by spawning too many workers for your machine/scenario.

https://stackoverflow.com/questions/17861362/node-js-child-process-difference-between-spawn-fork









유의사항

기본 multiprocessing을 사용하면, lambda function을 pickling할 수 없다.

또한 __new__함수에서 추가적인 variable을 받을 경우에도 pickle에 실패할 수 있다.


https://stackoverflow.com/questions/51895517/new-missing-1-required-positional-argument-depending-of-url-scraped






댓글