Python 멀티프로세싱

Python 멀티프로세싱 (Python Multiprocessing)

이 문서는 vLLM이 내부적으로 Python multiprocessing을 어떻게 다루는지 설명합니다. vLLM을 라이브러리로 사용하거나 특정 의존성과 함께 쓸 때 어떤 멀티프로세싱 방식(방법/메서드)을 고를지, 그리고 그 선택이 왜 까다로운지 살펴봅니다.

출처: 문서

본문

디버깅 (Debugging)

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

소개 (Introduction)

중요: 소스 코드 참조는 문서 작성 시점(2024년 12월)의 코드 상태를 기준으로 합니다.

vLLM에서 Python 멀티프로세싱을 쓰는 일은 다음 두 가지 때문에 복잡합니다.

  • vLLM을 라이브러리로 사용하면 내부 코드를 마음대로 제어할 수 없다.
  • 특정 멀티프로세싱 방식과 vLLM 의존성 사이에 비호환성이 있다.

이 문서는 vLLM이 이런 문제를 어떻게 해결하는지 설명합니다.

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

Python 멀티프로세싱 방식에는 다음이 있습니다.

  • spawn — 새로운 Python 프로세스를 생성합니다. Windows와 macOS의 기본값입니다.
  • forkos.fork()로 Python 인터프리터를 포크합니다. Python 3.14 이전 버전의 Linux 기본값입니다.
  • forkserver — 요청이 있을 때 새 프로세스를 포크할 서버 프로세스를 생성합니다. Python 3.14 이상의 Linux 기본값입니다.

방식별 장단점 (Tradeoffs)

fork는 가장 빠른 방식이지만 스레드를 사용하는 의존성과 호환되지 않습니다. macOS에서 fork를 쓰면 프로세스가 크래시할 수 있습니다.

spawn은 의존성과의 호환성이 더 좋지만, vLLM을 라이브러리로 쓸 때 문제가 될 수 있습니다. 사용하는 코드에 __main__ 가드(if __name__ == "__main__":)가 없으면, vLLM이 새 프로세스를 spawn할 때 코드가 의도치 않게 다시 실행되어 무한 재귀 등이 발생할 수 있습니다.

forkserver는 요청 시 새 프로세스를 포크할 서버 프로세스를 생성합니다. 안타깝게도 vLLM을 라이브러리로 쓸 때는 spawn과 같은 문제가 있습니다. 서버 프로세스 자체가 spawn된 새 프로세스로 생성되기 때문에 __main__ 가드로 보호되지 않은 코드가 다시 실행되기 때문입니다.

spawnforkserver 모두 fork처럼 전역 상태를 상속받는 것에 의존해서는 안 됩니다.

의존성과의 호환성 (Compatibility with Dependencies)

여러 vLLM 의존성이 spawn을 선호하거나 요구합니다.

이런 의존성을 초기화한 뒤 fork를 쓰면 알려진 문제가 발생합니다.

현재 상태 (v0) (Current State (v0))

환경 변수 VLLM_WORKER_MULTIPROC_METHOD로 vLLM이 사용할 방식을 제어할 수 있습니다. 현재 기본값은 fork입니다.

메인 프로세스가 vllm 커맨드로 제어되면 spawn이 사용됩니다(가장 폭넓게 호환되기 때문입니다).

multiproc_xpu_executor는 반드시 spawn을 사용하도록 강제합니다.

그 외에도 곳곳에 spawn을 하드코딩한 위치가 있습니다.

관련 PR: Pull Request #8823

v1에서의 이전 상태 (Prior State in v1)

v1 엔진 코어에서 멀티프로세싱을 사용할지 제어하는 환경 변수 VLLM_ENABLE_V1_MULTIPROCESSING이 있었고, 기본값은 꺼짐(off)이었습니다.

활성화하면 v1 LLMEngine이 엔진 코어를 실행하기 위해 새 프로세스를 생성했습니다.

위에서 언급한 모든 이유(의존성 호환성, vLLM을 라이브러리로 쓰는 코드) 때문에 기본값은 꺼져 있었습니다.

v1에서 변경한 내용 (Changes Made in v1)

Python의 multiprocessing으로 모든 환경에서 동작하는 쉬운 해법은 없습니다. 첫 단계로, v1은 호환성을 최대화하도록 멀티프로세싱 방식을 "최선의 노력(best effort)"으로 고르는 상태가 되도록 했습니다.

  • 기본값은 fork.
  • 메인 프로세스를 우리가 제어한다는 걸 알 때(vllm 실행) spawn 사용.
  • cuda가 이전에 초기화된 것이 감지되면 spawn을 강제하고 경고를 출력. fork가 깨질 것을 알기에 할 수 있는 최선입니다.

이 시나리오에서 여전히 깨지는 것으로 알려진 경우는, vLLM을 라이브러리로 쓰는 코드가 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.

둘째, Python 자체가 친절한 설명과 함께 예외를 일으킵니다:

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

고려했던 대안들 (Alternatives Considered)

__main__ 가드 존재 감지 (Detect if a main guard is present)

vLLM을 라이브러리로 쓰는 코드에 __main__ 가드가 있는지 감지할 수 있다면 더 잘 동작할 수 있지 않겠냐는 제안이 있었습니다. 이 Stack Overflow 게시물은 같은 질문을 마주한 라이브러리 저자의 사례입니다.

원래 __main__ 프로세스에 있는지, 아니면 이후 spawn된 프로세스에 있는지는 감지할 수 있습니다. 하지만 코드에 __main__ 가드가 있는지를 감지하는 것은 간단해 보이지 않습니다. 이 옵션은 비현실적이라 폐기되었습니다.

forkserver 사용 (Use forkserver)

처음에는 forkserver가 좋은 해법처럼 보입니다. 하지만 그 동작 방식은 vLLM을 라이브러리로 쓸 때 spawn이 겪는 것과 같은 문제를 안고 있습니다.

항상 spawn 강제 (Force spawn all the time)

정리하는 한 가지 방법은 항상 spawn을 강제하고, vLLM을 라이브러리로 쓸 때 __main__ 가드 사용을 요구한다고 문서화하는 것입니다. 안타깝게도 이는 기존 코드를 깨뜨리고 vLLM을 쓰기 어렵게 만들어, LLM 클래스를 가능한 한 쉽게 쓰게 하겠다는 바람에 어긋납니다. 이런 부담을 사용자에게 떠넘기는 대신, 우리가 최선을 다해 동작하게 만드는 복잡성을 지니기로 했습니다.

향후 작업 (Future Work)

앞으로는 이런 문제를 우회하는 다른 워커 관리 방식을 고려할 수 있습니다.

  • forkserver와 비슷하지만, 프로세스 매니저를 우리가 직접 subprocess와 커스텀 엔트리포인트로 처음에 띄우는 방식(vllm-manager 프로세스 실행)을 구현할 수 있습니다.
  • 우리 필요에 더 잘 맞는 다른 라이브러리를 탐색할 수 있습니다. 고려할 예시: https://github.com/joblib/loky

더 알아보기 (Learn more)