Python 멀티프로세싱

Python 멀티프로세싱 (Multiprocessing)

vLLM은 내부적으로 파이썬의 multiprocessing을 활용해 여러 프로세스로 작업을 나눠요. 그런데 이걸 실제로 써 보면 생각보다 까다로운데, 그 이유가 크게 두 가지예요. 첫째, vLLM을 라이브러리로 사용하다 보니 내부 코드를 마음대로 제어하기 어렵고, 둘째, 특정 멀티프로세싱 방식이 vLLM의 여러 의존성과 충돌하는 경우가 있거든요. 이 문서는 vLLM이 이런 제약을 어떻게 다루는지를 설명해요.

출처: 공식문서

디버깅

알려진 이슈와 해결 방법은 Troubleshooting 페이지를 참고하세요.

멀티프로세싱 방식 (Multiprocessing Methods)

파이썬의 multiprocessing 방식은 크게 셋으로 나뉘어요.

  • spawn — 새로운 파이썬 프로세스를 하나 띄워요. Windows와 macOS에서 기본값으로 쓰여요.
  • forkos.fork()로 파이썬 인터프리터 자체를 복제해요. 파이썬 3.14 이전 버전의 Linux에서 기본값이에요.
  • forkserver — 서버 프로세스를 하나 띄워 두고, 요청이 올 때마다 그 서버가 새 프로세스를 fork 해 줘요. 파이썬 3.14 이상 Linux에서 기본값이에요.

각 방식의 장단점 (Tradeoffs)

fork가 가장 빠르지만, 스레드를 쓰는 의존성과는 호환이 안 돼요. macOS에서 fork를 쓰면 프로세스가 크래시날 수 있어요.

spawn은 의존성과의 호환성이 좋은 대신, vLLM을 라이브러리로 쓸 때 문제가 될 수 있어요. 호출하는 코드에 __main__ 가드(if __name__ == "__main__":)가 없으면, vLLM이 새 프로세스를 띄울 때 그 코드가 무심코 다시 실행돼요. 이러면 무한 재귀 같은 문제가 생길 수 있거든요.

forkserver는 요청이 있을 때만 fork 해 주는 서버 프로세스를 띄워요. 그런데 아쉽게도 vLLM을 라이브러리로 쓸 때는 spawn과 똑같은 문제가 생겨요. 서버 프로세스 자체가 새로 spawn 된 프로세스라서, __main__ 가드로 보호되지 않은 코드를 다시 실행하게 되거든요.

spawnforkserver는 둘 다, fork처럼 전역 상태를 이어받는 것에 의존하면 안 돼요.

의존성과의 호환성

여러 vLLM 의존성들이 spawn을 권장하거나 요구해요.

아래 링크에서 그 근거를 볼 수 있어요.

이런 의존성들을 초기화한 뒤에 fork를 쓰면 알려진 문제가 발생해요.

현재 상태 (v0)

환경변수 VLLM_WORKER_MULTIPROC_METHOD로 vLLM이 어떤 방식을 쓸지 제어할 수 있어요. 기본값은 현재 fork예요.

메인 프로세스가 vllm 커맨드로 제어되는 경우에는 가장 호환성이 넓은 spawn을 써요.

multiproc_xpu_executor는 강제로 spawn을 쓰게 돼 있어요.

그 외에도 spawn을 하드코딩해 쓰는 곳이 몇 군데 더 있어요.

관련 PR:

v1에서의 이전 상태

v1 엔진 코어에서 멀티프로세싱을 쓸지 제어하는 환경변수 VLLM_ENABLE_V1_MULTIPROCESSING이 있었어요. 기본값은 꺼짐이었죠.

이걸 켜면 v1 LLMEngine이 엔진 코어를 돌리기 위해 새 프로세스를 만들었어요.

기본값이 꺼짐이었던 이유는 앞에서 말한 그 문제들 때문이었어요 — 의존성과의 호환성, 그리고 vLLM을 라이브러리로 쓰는 코드 때문이었죠.

v1에서 바꾼 점

파이썬의 multiprocessing을 모든 환경에서 완벽하게 동작하게 만드는 쉬운 해법은 없어요. 그래서 첫 단계로, v1을 "최선을 다해(best effort)" 멀티프로세싱 방식을 골라 호환성을 최대한 끌어올리는 상태로 만들었어요.

  • 기본적으로는 fork를 써요.
  • 메인 프로세스를 우리가 제어한다는 걸 알면(vllm이 실행된 경우) spawn을 써요.
  • cuda가 이미 초기화된 걸 감지하면 spawn을 강제하고 경고를 띄워요. fork가 어차피 깨질 걸 알고 있으니, 이게 우리가 할 수 있는 최선이에요.

이 시나리오에서 여전히 깨지는 걸로 알려진 경우는, vLLM을 호출하기 전에 cuda를 먼저 초기화하는 라이브러리 사용 코드예요. 이때 띄우는 경고는 사용자에게 __main__ 가드를 추가하거나 멀티프로세싱을 끄라고 안내해요.

그 알려진 실패 케이스가 발생하면, 사용자는 상황을 설명하는 메시지 두 개를 보게 돼요. 첫 번째는 vLLM의 로그 메시지예요.

WARNING 12-11 14:50:37 multiproc_worker_utils.py:281] CUDA was previously
    initialized. We must use the `spawn` multiprocessing start method. Setting
    VLLM_WORKER_MULTIPROC_METHOD to 'spawn'. See
    https://docs.vllm.ai/en/latest/usage/troubleshooting.html#python-multiprocessing
    for more information.

두 번째는 파이썬 자체가 던지는 예외예요. 내용이 친절하게 설명돼 있어요.

RuntimeError:
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.

        To fix this issue, refer to the "Safe importing of main module"
        section in https://docs.python.org/3/library/multiprocessing.html

고려했던 대안들

__main__ 가드가 있는지 감지하기

vLLM을 라이브러리로 쓰는 코드에 __main__ 가드가 있는지 감지하면 더 잘 대응할 수 있지 않을까 하는 제안이 있었어요. 같은 고민을 겪은 라이브러리 저자의 Stack Overflow 포스트도 있었죠.

원래 __main__ 프로세스인지, 아니면 이후에 spawn 된 프로세스인지는 감지할 수 있어요. 그런데 코드에 __main__ 가드가 있는지는 감지하기가 간단해 보이지 않아요. 그래서 이 옵션은 실용성이 없다고 판단해 버렸어요.

forkserver 쓰기

처음 보기엔 forkserver가 좋은 해법처럼 보여요. 그런데 동작 방식이 vLLM을 라이브러리로 쓸 때 spawn과 똑같은 문제를 일으켜요.

항상 spawn 강제하기

깔끔하게 정리하는 방법 하나는, 아예 항상 spawn을 강제하고 "vLLM을 라이브러리로 쓸 때는 __main__ 가드가 필요하다"고 문서화하는 거예요. 그런데 이러면 기존 코드를 깨뜨리고 vLLM을 쓰기 더 어렵게 만들어서, LLM 클래스를 최대한 쉽게 쓰게 하려는 목표와 어긋나요. 그래서 이 부담을 사용자에게 떠넘기는 대신, 우리 쪽에서 복잡성을 안고 최선을 다해 동작하게 만들기로 했어요.

앞으로의 작업

나중에는 이런 제약을 우회하는 다른 워커 관리 방식을 고려해 볼 수 있어요.

  1. forkserver와 비슷하지만, 프로세스 매니저를 우리가 처음에 직접 띄우는 방식으로 구현할 수 있어요 — 자체 서브프로세스와 워커 관리를 위한 커스텀 엔트리포인트(vllm-manager 프로세스 실행)를 쓰는 방식이요.

  2. 우리 요구에 더 잘 맞는 다른 라이브러리를 탐색해 볼 수 있어요. 예를 들면요.

더 알아보기 (Learn more)