병렬 처리와 확장

병렬 처리와 확장 (Parallelism and Scaling)

단일 모델 레플리카를 서빙할 때, 모델이 한 GPU에 안 들어가면 어떻게 해야 할까요? vLLM을 처음 다루다 보면 "모델이 너무 커서 GPU 메모리에 안 들어와요"라는 문제를 만나게 되죠. 이 문서는 그럴 때 어떤 분산 추론 전략을 골라야 하는지, 그리고 GPU를 여러 대 노드에 걸쳐 어떻게 배치하는지 설명해요.

출처: vLLM 공식 문서 — parallelism_scaling

단일 모델 레플리카를 위한 분산 추론 전략

단일 모델 레플리카에 어떤 분산 추론 전략을 쓸지 고를 때는 다음 기준을 따라가면 돼요.

  • 단일 GPU (분산 없음): 모델이 GPU 하나에 들어간다면 분산 추론은 사실 필요 없어요. 그 GPU 하나에서 그냥 추론을 돌리면 됩니다.
  • 단일 노드 멀티 GPU, 텐서 병렬: 모델이 GPU 하나에는 너무 크지만 한 노드 안의 여러 GPU에는 들어간다면 *텐서 병렬(tensor parallelism)*을 써요. 예를 들어 GPU가 4개 있는 노드라면 tensor_parallel_size=4로 설정하는 식이죠.
  • 멀티 노드 멀티 GPU, 텐서 병렬 + 파이프라인 병렬: 모델이 한 노드에도 너무 크다면 텐서 병렬과 *파이프라인 병렬(pipeline parallelism)*을 조합해요. tensor_parallel_size에는 노드당 GPU 수를, pipeline_parallel_size에는 노드 수를 설정합니다. 예를 들어 GPU가 8개씩 있는 노드 2개를 쓴다면 tensor_parallel_size=8, pipeline_parallel_size=2로 잡는 거예요.

모델이 들어갈 만큼 GPU 메모리가 확보될 때까지 GPU와 노드 수를 늘리고, tensor_parallel_size는 노드당 GPU 수, pipeline_parallel_size는 노드 수로 맞춰 주면 돼요.

리소스를 충분히 준비한 뒤 vllm을 실행하면 아래 같은 로그 메시지를 볼 수 있어요.

INFO 07-23 13:56:04 [kv_cache_utils.py:775] GPU KV cache size: 643,232 tokens
INFO 07-23 13:56:04 [kv_cache_utils.py:779] Maximum concurrency for 40,960 tokens per request: 15.70x

GPU KV cache size 줄은 GPU KV 캐시에 동시에 저장할 수 있는 총 토큰 수를 알려줘요. Maximum concurrency 줄은 각 요청이 지정된 토큰 수(위 예시에서는 40,960)를 사용한다고 할 때 동시에 서빙할 수 있는 요청 수의 추정치를 보여줍니다. 요청당 토큰 수는 모델 설정의 최대 시퀀스 길이인 ModelConfig.max_model_len에서 가져와요. 이 숫자들이 요구하는 처리량보다 낮다면 클러스터에 GPU나 노드를 더 추가하면 됩니다.

엣지 케이스: GPU 분할이 고르지 않을 때

모델이 한 노드에 들어가긴 하는데 GPU 수가 모델 크기와 정확히 나누어떨어지지 않는다면, 파이프라인 병렬을 켜면 돼요. 파이프라인 병렬은 모델을 레이어 단위로 쪼개기 때문에 고르지 않은 분할도 지원하거든요. 이 경우 tensor_parallel_size=1로 두고 pipeline_parallel_size에는 GPU 수를 설정합니다. 또 노드의 GPU들이 NVLINK로 연결되어 있지 않다면(예: L40S), 높은 처리량과 낮은 통신 오버헤드를 위해 텐서 병렬 대신 파이프라인 병렬을 쓰는 게 좋아요.

Mixture of Experts (MoE) 모델의 분산 서빙

MoE 모델은 전문가(experts)의 내재된 병렬성을 활용하는 것이 유리할 때가 많아요. 전문가 레이어에 별도의 병렬 전략을 쓰면 되죠. vLLM은 Data Parallel 어텐션과 Expert/Tensor Parallel MoE 레이어를 결합한 대규모 배포를 지원합니다. 자세한 내용은 Data Parallel 배포 문서를 참고하세요.

단일 노드 배포

vLLM은 분산 텐서 병렬 및 파이프라인 병렬 추론·서빙을 지원해요. 구현에는 Megatron-LM의 텐서 병렬 알고리즘이 포함됩니다.

기본 분산 런타임은 멀티 노드 추론에 Ray, 단일 노드 추론에 네이티브 Python multiprocessing이에요. 기본값은 LLM 클래스에서 distributed_executor_backend를 설정하거나 API 서버에서 --distributed-executor-backend로 바꿀 수 있어요. multiprocessing을 쓰려면 mp, Ray를 쓰려면 ray를 지정하면 됩니다.

멀티 GPU 추론을 하려면 LLM 클래스의 tensor_parallel_size를 원하는 GPU 수로 설정해요. 예를 들어 4개 GPU에서 추론을 돌리려면:

from vllm import LLM
llm = LLM("facebook/opt-13b", tensor_parallel_size=4)
output = llm.generate("San Francisco is a")

멀티 GPU 서빙을 하려면 서버를 시작할 때 --tensor-parallel-size를 포함시켜요. 4개 GPU에서 API 서버를 돌리는 예시:

vllm serve facebook/opt-13b \
     --tensor-parallel-size 4

파이프라인 병렬을 켜려면 --pipeline-parallel-size를 추가하면 됩니다. 텐서 병렬과 파이프라인 병렬을 함께 써서 8개 GPU에서 API 서버를 돌리는 예시:

# Eight GPUs total
vllm serve gpt2 \
     --tensor-parallel-size 4 \
     --pipeline-parallel-size 2

멀티 노드 배포

한 노드에 모델을 담을 GPU가 부족하다면 vLLM을 여러 노드에 걸쳐 배포해요. 이때 모든 노드가 동일한 실행 환경(모델 경로와 Python 패키지 포함)을 제공해야 합니다. 컨테이너 이미지를 쓰는 게 환경을 일관되게 유지하고 호스트 간 이질성을 감추는 편리한 방법이라 권장해요.

Ray란 무엇인가?

Ray는 Python 프로그램을 확장하기 위한 분산 컴퓨팅 프레임워크예요. 멀티 노드 vLLM 배포는 Ray를 런타임 엔진으로 사용할 수 있습니다.

vLLM은 Ray를 사용해 여러 노드에 걸친 작업의 분산 실행을 관리하고 실행 위치를 제어해요.

Ray는 또한 vLLM을 엔진으로 활용할 수 있는 대규모 오프라인 배치 추론온라인 서빙을 위한 고수준 API를 제공합니다. 이 API들은 vLLM 워크로드에 프로덕션급 장애 허용(fault tolerance), 확장, 분산 관측성을 더해줘요.

Ray는 선택적 의존성이라서, Ray 기반 실행을 쓰기 전에 명시적으로 설치해야 해요. 예:

pip install "ray[cgraph]"

자세한 내용은 Ray 문서를 참고하세요.

컨테이너로 Ray 클러스터 구성하기

헬퍼 스크립트 examples/ray_serving/run_cluster.sh는 여러 노드에 컨테이너를 시작하고 Ray를 초기화해요. 기본적으로 이 스크립트는 관리자 권한 없이 Docker를 실행하기 때문에, 프로파일링이나 트레이싱 시 GPU 성능 카운터에 접근할 수 없어요. 관리자 권한을 켜려면 Docker 명령에 --cap-add=CAP_SYS_ADMIN 플래그를 추가하면 됩니다.

노드 하나를 헤드 노드로 정하고 실행:

bash run_cluster.sh \
                vllm/vllm-openai \
                <HEAD_NODE_IP> \
                --head \
                /path/to/the/huggingface/home/in/this/node \
                -e VLLM_HOST_IP=<HEAD_NODE_IP>

각 워커 노드에서 실행:

bash run_cluster.sh \
                vllm/vllm-openai \
                <HEAD_NODE_IP> \
                --worker \
                /path/to/the/huggingface/home/in/this/node \
                -e VLLM_HOST_IP=<WORKER_NODE_IP>

VLLM_HOST_IP는 워커마다 고유하다는 점을 기억하세요. 이 명령들을 실행 중인 셸을 계속 열어 두어야 해요. 셸 하나라도 닫으면 클러스터가 종료됩니다. 그리고 모든 노드가 IP 주소를 통해 서로 통신할 수 있어야 합니다.

네트워크 보안: 보안을 위해 VLLM_HOST_IP는 사설 네트워크 세그먼트의 주소로 설정하세요. 이 네트워크를 통해 보내는 트래픽은 암호화되지 않고, 엔드포인트가 주고받는 데이터 형식은 공격자가 네트워크에 접근할 경우 임의 코드를 실행하는 데 악용될 수 있어요. 신뢰할 수 없는 당사자가 네트워크에 닿지 못하게 하세요.

어느 노드에서든 컨테이너에 들어가 ray statusray list nodes를 실행하면 Ray가 예상한 수의 노드와 GPU를 찾는지 확인할 수 있어요.

팁: KubeRay를 사용해 Ray 클러스터를 구성할 수도 있습니다. 자세한 내용은 KubeRay vLLM 문서를 참고하세요.

Ray 클러스터에서 vLLM 실행하기

팁: Ray가 컨테이너 안에서 실행 중이라면, 이 가이드의 나머지 명령들을 호스트가 아니라 컨테이너 안에서 실행하세요. 컨테이너 안에서 셸을 열려면 노드에 연결한 뒤 docker exec -it <container_name> /bin/bash를 쓰면 됩니다.

Ray 클러스터가 실행되면 단일 노드 설정에서와 똑같이 vLLM을 쓰면 돼요. Ray 클러스터의 모든 리소스가 vLLM에 보이므로, 한 노드에서 vllm 명령을 한 번만 실행하면 충분합니다.

일반적인 관행은 텐서 병렬 크기를 각 노드의 GPU 수로, 파이프라인 병렬 크기를 노드 수로 맞추는 거예요. 예를 들어 2개 노드에 걸쳐 16개 GPU(노드당 8개)가 있다면 텐서 병렬 크기 8, 파이프라인 병렬 크기 2로 설정합니다:

vllm serve /path/to/the/model/in/the/container \
    --tensor-parallel-size 8 \
    --pipeline-parallel-size 2 \
    --distributed-executor-backend ray

또는 tensor_parallel_size를 클러스터의 총 GPU 수로 설정할 수도 있어요:

vllm serve /path/to/the/model/in/the/container \
     --tensor-parallel-size 16 \
     --distributed-executor-backend ray

multiprocessing으로 vLLM 실행하기

Ray 외에도 멀티 노드 vLLM 배포는 multiprocessing을 런타임 엔진으로 쓸 수 있어요. tp_size=8, pp_size=2로 2개 노드(노드당 8개 GPU)에 모델을 배포하는 예시를 볼게요.

노드 하나를 헤드 노드로 정하고 실행:

vllm serve /path/to/the/model/in/the/container \
  --tensor-parallel-size 8 --pipeline-parallel-size 2 \
  --nnodes 2 --node-rank 0 \
  --master-addr <HEAD_NODE_IP>

다른 워커 노드에서 실행:

vllm serve /path/to/the/model/in/the/container \
  --tensor-parallel-size 8 --pipeline-parallel-size 2 \
  --nnodes 2 --node-rank 1 \
  --master-addr <HEAD_NODE_IP> --headless

텐서 병렬을 위한 네트워크 통신 최적화

효율적인 텐서 병렬은 빠른 노드 간 통신, 가능하면 InfiniBand 같은 고속 네트워크 어댑터를 필요로 해요. 클러스터가 InfiniBand를 쓰도록 설정하려면 examples/ray_serving/run_cluster.sh 헬퍼 스크립트에 --privileged -e NCCL_IB_HCA=mlx5 같은 인자를 추가하면 됩니다. 필요한 플래그에 대한 자세한 내용은 시스템 관리자에게 문의하세요.

GPUDirect RDMA 활성화

GPUDirect RDMA(Remote Direct Memory Access)는 네트워크 어댑터가 CPU와 시스템 메모리를 거치지 않고 GPU 메모리에 직접 접근할 수 있게 해주는 NVIDIA 기술이에요. 이 직접 접근은 지연 시간과 CPU 오버헤드를 줄여주는데, 노드 간 GPU 데이터 전송이 클 때 유용하죠.

vLLM과 함께 GPUDirect RDMA를 켜려면 다음을 설정하세요.

  • IPC_LOCK 보안 컨텍스트: 메모리 페이지를 잠그고 디스크로 스왑되지 않도록 컨테이너 보안 컨텍스트에 IPC_LOCK 기능을 추가해요.
  • /dev/shm 공유 메모리: 프로세스 간 통신(IPC)을 위한 공유 메모리를 제공하도록 pod 스펙에 /dev/shm을 마운트합니다.

Docker를 쓴다면 다음과 같이 컨테이너를 설정하세요.

docker run --gpus all \
    --ipc=host \
    --shm-size=16G \
    -v /dev/shm:/dev/shm \
    vllm/vllm-openai

Kubernetes를 쓴다면 pod 스펙을 다음과 같이 설정합니다.

...
spec:
  containers:
    - name: vllm
      image: vllm/vllm-openai
      securityContext:
        capabilities:
          add: ["IPC_LOCK"]
      volumeMounts:
        - mountPath: /dev/shm
          name: dshm
      resources:
        limits:
          nvidia.com/gpu: 8
        requests:
          nvidia.com/gpu: 8
  volumes:
    - name: dshm
      emptyDir:
        medium: Memory
...

GPUDirect RDMA 동작 확인하기: InfiniBand 카드가 GPUDirect RDMA를 쓰는지 확인하려면 상세 NCCL 로그로 vLLM을 실행하면 돼요. NCCL_DEBUG=TRACE vllm serve ...처럼요. 그다음 로그에서 NCCL 버전과 사용된 네트워크를 찾아보세요.

  • 로그에서 [send] via NET/IB/GDRDMA를 찾으면 NCCL이 InfiniBand + GPUDirect RDMA를 사용하는 것이고, 이는 효율적이에요.
  • 로그에서 [send] via NET/Socket을 찾으면 NCCL이 원시 TCP 소켓을 쓴 것이고, 이는 노드 간 텐서 병렬에 효율적이지 않아요.

Hugging Face 모델 미리 다운로드: Hugging Face 모델을 쓴다면 vLLM을 시작하기 전에 모델을 다운로드해 두는 걸 권장해요. 모든 노드의 같은 경로에 모델을 다운로드하거나, 모든 노드가 접근할 수 있는 분산 파일 시스템에 모델을 저장하세요. 그다음 repository ID 대신 모델 경로를 넘겨주면 됩니다. 그렇지 않으면 run_cluster.sh-e HF_TOKEN=<TOKEN>을 추가해 Hugging Face 토큰을 제공하세요.

분산 배포 문제 해결

분산 디버깅에 대한 정보는 분산 배포 문제 해결 문서를 참고하세요.

더 알아보기 (Learn more)