Multiprocessing 가이드
Process
Pool
Queue
Pipe
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()
따라서 python에서는 일반적으로 thread 대신 multiprocessing을 사용한다
그런데 multiprocessing을 사용하게 되면 shared memory를 사용하기 힘들다는 단점이 존재한다.
그래서 이를 해결하기 위해 pickle/unpickle 등의 트릭을 이용해서 SHM을 사용하기도 하는데, 이는 공유할 데이터가 커지면 속도가 매우 느려지는 단점이 존재한다.
그대신 python에서 제공하는 sharedctypes를 사용하거나, multiprocessing.Value 나 Array를 사용하는 방법이 존재한다.
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를 가져올 수 있다.
Multi-process Graceful Termination
https://cuyu.github.io/python/2016/08/15/Terminate-multiprocess-in-Python-correctly-and-gracefully
Pytorch Multiprocessing
하나의 네트워크를 두개의 process에서 사용/업데이트 해야하는 경우가 torch.multiporcessing의 대표적인 예시라고 할 수 있다.
Queue를 통해 tensor를 다른 process로 보낼 경우, 이 데이터들은 전부 shared memory로 옮겨지고, 그에 대한 handle을 서로 공유하게 된다. 이는 Gradient또한 동일하다.
API 문서
오직 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가 이상하게 실행되는 경우
이는 python main.py에서는 if __name__ == '__main__': 으로 보호가 되어있더라도 실제로 process가 시작되는 file이 따로 있는 경우 이런 문제가 생기는 것으로 보인다.
즉 entry file에 process가 정의되어있지 않으면 이런 문제가 생기는 것으로 보인다.
SimpleQueue의 사용
Mutex와 Semaphore의 차이
Spawn과 fork의 차이
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에 실패할 수 있다.
'Development > Python' 카테고리의 다른 글
CSV파일 인코딩(Encoding) (0) | 2020.03.23 |
---|---|
Conda로 Python 버전 별 설치, 관리, 삭제하기 (0) | 2020.02.27 |
Python에서 directory 관련 명령어 (0) | 2020.02.19 |
Python 코드 안에서 git과 pip 사용하기 (0) | 2020.02.19 |
Advanced Python Scheduler (0) | 2020.02.18 |
댓글