Skip to main content

vLLM은 "모델을 올리는 도구"가 아니라 "서빙 엔진"이다

앞 장에서 우리는 프로덕션 추론 엔진을 비교했다. 이제 그중 가장 실전에서 자주 만나게 되는 도구, vLLM을 직접 다룰 차례다. vLLM을 한 문장으로 줄이면 "OpenAI 호환 API 서버를 띄우면서, GPU 메모리와 배치를 최대한 효율적으로 쓰도록 설계된 LLM 추론 엔진"이라고 할 수 있다.

여기서 중요한 단어는 API 서버도, 추론도 아니다. 효율이다. 대형 언어모델을 로컬이나 사내 서버에서 구동해 보면, 모델 가중치만 VRAM을 차지하는 것이 아니라는 사실을 금방 깨닫게 된다. 프롬프트를 읽는 동안 만들어지는 KV 캐시, 동시에 들어오는 요청, 긴 컨텍스트, CUDA 런타임과 vLLM 내부 버퍼까지 모두 VRAM을 사용한다. 그래서 "이론상 48GB면 되겠는데?"라고 생각한 모델이 실제로는 48GB GPU 두 장에서도 간당간당한 일이 생긴다.

vLLM이 빛나는 지점이 바로 여기다. vLLM은 PagedAttention, 연속 배칭, 병렬 실행 전략을 이용해 같은 GPU에서도 더 많은 요청을 처리하도록 도와준다. 특히 단일 노드에 GPU가 2장, 4장, 8장 있는 서버라면 --tensor-parallel-size 하나만으로도 대형 모델을 여러 GPU에 나누어 올릴 수 있다. vLLM 공식 문서도 단일 노드에서 모델이 한 장의 GPU에 들어가지 않지만 여러 GPU를 합치면 들어가는 경우, 우선 텐서 병렬을 사용하라고 안내한다.1

하지만 여기서 함정이 있다. 옵션 하나로 시작할 수 있다고 해서, 옵션 하나로 끝나는 것은 아니다. vLLM은 잘 설정하면 멀티 GPU 입문을 쉽게 만들어 주지만, 아무 생각 없이 기본값으로 띄우면 다음과 같은 메시지를 마주할 수 있다.

CUDA out of memory
Sequence group is preempted by PreemptionMode.RECOMPUTE
NCCL error
GPU KV cache size: ... tokens
Maximum concurrency for ... tokens per request: ...x

이 장의 목표는 이 메시지들을 무서워하지 않게 만드는 것이다. 우리는 2장부터 8장까지 쌓아 온 VRAM 계산, GPU 인터커넥트, 병렬화 전략, 추론 엔진 비교 지식을 바탕으로, vLLM에서 실제로 어떤 옵션을 어떻게 조합해야 하는지 살펴볼 것이다.

vLLM으로 단일 노드 멀티 GPU 서빙하기

먼저 가장 작은 성공부터 만든다

멀티 GPU 서버라고 해서 처음부터 모든 GPU를 다 쓰는 명령을 실행할 필요는 없다. 운영 환경에서 가장 나쁜 습관은 "큰 명령을 한 번에 실행하고, 실패하면 어디서 터졌는지 모르는 것"이다. vLLM은 모델 다운로드, 토크나이저 로딩, CUDA 초기화, NCCL 통신, 병렬 워커 생성, KV 캐시 프로파일링을 차례로 수행한다. 이 중 어느 단계에서든 문제가 생길 수 있다.

따라서 순서는 단순해야 한다. 먼저 GPU가 보이는지 확인한다.

nvidia-smi

그다음 vLLM이 설치되어 있는지 확인한다.

python -c "import vllm; print(vllm.__version__)"

Hugging Face의 gated model, 예를 들어 Llama 계열처럼 사용 승인이 필요한 모델을 쓴다면 토큰도 먼저 설정한다.

export HF_TOKEN=hf_xxxxxxxxxxxxxxxxx

그리고 처음에는 너무 긴 컨텍스트를 욕심내지 말고, 작은 --max-model-len으로 시작한다.

vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --dtype auto \
  --max-model-len 8192

이 명령이 성공하면 OpenAI 호환 API 서버가 기본적으로 8000번 포트에서 뜬다. 확인은 간단하다.

curl http://localhost:8000/v1/models

단일 GPU에서 정상 동작을 확인했다면, 이제 멀티 GPU로 넘어간다. 예를 들어 GPU 2장에 70B 모델을 나누어 올리는 가장 기본적인 형태는 다음과 같다.

CUDA_VISIBLE_DEVICES=0,1 \
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 2 \
  --dtype bfloat16 \
  --max-model-len 8192

CUDA_VISIBLE_DEVICES=0,1은 vLLM에게 0번과 1번 GPU만 보이게 하는 장치다. 서버에 GPU가 4장 있어도 특정 작업에는 2장만 쓰고 싶을 때 가장 깔끔한 방법이다. vLLM 공식 문서도 CUDA 초기화 문제를 피하려면 코드 안에서 장치를 직접 건드리기보다 CUDA_VISIBLE_DEVICES로 사용할 GPU를 제한하는 방식을 권장한다.2

Docker로 실행한다면 공유 메모리 설정을 빼먹지 않는 것이 좋다. 멀티 프로세스와 GPU 통신이 얽히면 /dev/shm 부족이나 IPC 문제로 애매한 오류가 날 수 있다.

docker run --gpus all \
  --ipc=host \
  --network host \
  -e HF_TOKEN=$HF_TOKEN \
  vllm/vllm-openai:latest \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 2 \
  --dtype bfloat16 \
  --max-model-len 8192

여기까지가 출발점이다. 이제 진짜 중요한 질문으로 들어가자. GPU가 여러 장 있을 때, --tensor-parallel-size, --pipeline-parallel-size, --data-parallel-size를 어떻게 조합해야 할까?

네 가지 병렬 옵션을 한 번에 이해하기

vLLM에서 멀티 GPU를 다룰 때 가장 자주 만나는 옵션은 다음 네 가지다.

옵션의미주된 목적모델을 여러 GPU에 나누는가?
--tensor-parallel-size한 레이어 내부의 행렬 연산을 GPU 여러 장에 분할큰 모델 적재, 낮은 지연시간예
--pipeline-parallel-size모델의 레이어 묶음을 단계별 GPU에 분할PCIe 환경, 멀티 노드, 불균등 분할예
--data-parallel-size모델 복제본을 여러 개 띄워 요청을 나눠 처리처리량 확장아니오, 복제함
--enable-expert-parallelMoE 모델의 전문가 레이어를 GPU에 분산MoE 모델 효율화MoE 전문가에 한해 예

이 표를 머릿속에 넣어 두면 vLLM 명령이 훨씬 덜 복잡해 보인다. 핵심은 TP와 PP는 모델 하나를 쪼개는 방법이고, DP는 모델 복제본을 늘리는 방법이라는 점이다.

텐서 병렬: 한 레이어를 여러 GPU가 동시에 계산한다

--tensor-parallel-size, 줄여서 TP는 대형 모델을 vLLM에 올릴 때 가장 먼저 쓰는 옵션이다.

vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4

TP=4라면 각 GPU가 모델 레이어 내부의 일부 가중치와 연산을 맡는다. 예를 들어 거대한 행렬 곱셈이 있을 때, 4장의 GPU가 각각 조각을 계산하고 결과를 합친다. 이때 GPU들은 레이어마다 서로 결과를 교환해야 한다. 이 통신에는 NCCL이 사용된다. NCCL은 NVIDIA GPU 간 통신을 담당하는 라이브러리라고 이해하면 된다.

TP의 장점은 모든 GPU가 동시에 한 토큰의 계산에 참여하므로 단일 요청 지연시간을 비교적 낮게 유지할 수 있다는 것이다. 그러나 단점도 분명하다. 레이어마다 통신이 필요하다. NVLink가 있는 서버에서는 이 통신 비용이 비교적 작지만, PCIe만 있는 서버에서는 생각보다 큰 병목이 된다.

또 하나의 실무적 조건이 있다. TP 크기는 모델 구조와 잘 맞아야 한다. 특히 attention head 수와 나누어떨어지지 않는 조합에서는 실행이 안 되거나 비효율적일 수 있다. 보통 1, 2, 4, 8처럼 GPU 수와 모델 구조에 맞는 값을 사용한다.

파이프라인 병렬: 모델의 층을 나누어 순서대로 통과시킨다

--pipeline-parallel-size, 줄여서 PP는 모델의 레이어를 여러 단계로 나눈다.

vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --pipeline-parallel-size 4

PP=4라면 80개 레이어를 가진 모델을 대략 4등분해서, GPU 0이 앞쪽 레이어, GPU 1이 그다음 레이어, GPU 2가 그다음, GPU 3이 마지막 레이어를 맡는 식이다. 마치 공장 컨베이어 벨트처럼 요청이 GPU 단계를 차례로 지나간다.

PP의 장점은 통신량이 TP보다 적다는 점이다. TP는 레이어 내부 계산 결과를 자주 동기화해야 하지만, PP는 한 단계의 출력만 다음 단계로 넘기면 된다. 그래서 NVLink가 없는 PCIe-only 서버에서는 PP가 더 나은 선택이 될 때가 있다. vLLM 공식 문서도 GPU가 NVLink로 연결되어 있지 않은 경우, 예를 들어 L40S 같은 PCIe 기반 구성에서는 TP보다 PP를 활용하는 것이 통신 오버헤드를 줄이는 데 도움이 될 수 있다고 설명한다.1

단점은 파이프라인 버블이다. 단일 요청만 들어오면 어떤 순간에는 GPU 하나만 바쁘고 나머지는 기다릴 수 있다. 따라서 PP는 동시 요청이 어느 정도 있어야 장점이 살아난다. 혼자 쓰는 챗봇보다 여러 사용자가 동시에 접속하는 API 서버에 더 잘 맞는다.

데이터 병렬: 모델을 쪼개지 않고 복제한다

--data-parallel-size는 전혀 다른 전략이다. 모델 하나를 여러 GPU에 나누는 것이 아니라, 모델 복제본을 여러 개 만든 뒤 요청을 나눠 보낸다.

예를 들어 8B 모델 하나가 GPU 1장에 충분히 들어가고, GPU가 4장 있다면 TP=4로 쪼개기보다 DP=4로 복제본 4개를 띄우는 편이 나을 수 있다.

vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --data-parallel-size 4

이 경우 각 GPU는 모델 전체를 가진다. 따라서 모델이 단일 GPU에 들어가지 않는 문제는 해결하지 못한다. 대신 여러 요청을 병렬로 처리해 총 처리량을 높인다. 쉽게 말해 TP와 PP가 "큰 냉장고 하나를 여러 칸으로 나눠 큰 음식을 넣는 방법"이라면, DP는 "같은 냉장고를 여러 대 사서 손님을 나누어 받는 방법"이다.

DP는 특히 모델이 작거나, 양자화 덕분에 단일 GPU에 잘 들어가거나, 여러 팀·여러 서비스 요청을 안정적으로 나누어 처리하고 싶을 때 유용하다. vLLM 최적화 문서도 DP를 전체 모델 복제 후 서로 다른 요청 배치를 병렬 처리하는 방식으로 설명한다.3

전문가 병렬: MoE 모델의 전문가를 나눈다

--enable-expert-parallel은 모든 모델에 쓰는 옵션이 아니다. Mixtral, DeepSeek 계열, Qwen MoE, Llama 4 계열처럼 MoE, 즉 Mixture of Experts 구조를 가진 모델에서 의미가 있다.

MoE 모델은 모든 파라미터가 매 토큰마다 활성화되지 않는다. 여러 "전문가" 네트워크 중 일부만 선택되어 계산에 참여한다. 그래서 총 파라미터 수는 거대하지만 활성 파라미터 수는 상대적으로 작다. 문제는 전문가 가중치 전체를 어디엔가는 올려야 한다는 점이다.

이때 expert parallel을 켜면 MoE 전문가 레이어를 GPU들에 분산할 수 있다.

vllm serve deepseek-ai/DeepSeek-V3 \
  --tensor-parallel-size 8 \
  --enable-expert-parallel

또는 처리량 중심의 MoE 서빙에서는 DP와 EP를 함께 고려한다.

vllm serve deepseek-ai/DeepSeek-V3 \
  --data-parallel-size 8 \
  --enable-expert-parallel

vLLM의 병렬화 문서에서는 MoE 모델에서 expert layer의 별도 병렬화 전략이 유리할 수 있으며, TP·DP와 결합해 대규모 배포를 구성할 수 있다고 설명한다.1 다만 MoE는 모델별 구조 차이가 크다. 다음 장에서 MoE와 전문가 병렬은 더 깊게 다룰 것이므로, 여기서는 "MoE가 아니면 일단 켜지 않는다" 정도로 기억해도 충분하다.

총 GPU 수 계산: 곱셈을 틀리면 서버가 안 뜬다

vLLM 병렬 옵션을 조합할 때 가장 먼저 확인해야 할 공식은 단순하다.

필요 GPU 수 = tensor_parallel_size × pipeline_parallel_size × data_parallel_size

--enable-expert-parallel은 보통 별도의 GPU 수를 추가로 곱하는 옵션이 아니라, MoE 레이어를 기존 병렬 그룹 안에서 어떻게 나눌지 바꾸는 옵션으로 이해하면 된다. 단, 버전과 모델에 따라 세부 구현이 바뀔 수 있으므로 MoE 모델에서는 반드시 vLLM 버전의 문서를 확인해야 한다.

예를 들어 단일 노드 8GPU 서버에서 다음 명령은 GPU 8장을 사용한다.

vllm serve meta-llama/Llama-3.1-405B-Instruct \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 2

TP=4, PP=2이므로 4×2=8이다. 여기에 --data-parallel-size 2까지 붙이면 16장이 필요하다. 단일 노드 8GPU 서버에서 실행하면 당연히 실패한다.

반대로 4GPU 서버에서 70B 모델을 FP16/BF16으로 올리고 싶다면 이런 식으로 시작할 수 있다.

CUDA_VISIBLE_DEVICES=0,1,2,3 \
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 8192

8GPU 서버에서 같은 모델을 더 넉넉하게 돌리고 싶다면 TP=8도 가능하다.

CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 8 \
  --dtype bfloat16 \
  --max-model-len 16384

하지만 GPU가 많다고 항상 TP를 크게 잡는 것이 정답은 아니다. 모델이 2장에 충분히 들어가는데 TP=8로 늘리면, 모델 가중치당 VRAM 여유는 늘어나겠지만 GPU 간 동기화 비용도 증가한다. 처리량이 올라갈 수도 있지만, 단일 요청 지연시간은 나빠질 수 있다. 멀티 GPU는 마법이 아니라 교환이다. VRAM 여유를 얻는 대신 통신 비용을 지불한다.

이제 가장 실전적인 의사결정으로 넘어가자. GPU가 여러 장 있는 서버에서 TP와 PP 중 무엇을 먼저 선택해야 할까?

vLLM 공식 가이드의 큰 방향은 명확하다. 모델이 단일 GPU에는 안 들어가지만 단일 노드의 여러 GPU에는 들어간다면 텐서 병렬을 사용한다. 모델이 단일 노드에도 안 들어가면 텐서 병렬과 파이프라인 병렬을 함께 사용한다.1 하지만 소비자용 워크스테이션이나 PCIe-only 서버에서는 한 가지 조건을 더 봐야 한다. GPU끼리 얼마나 빠르게 통신할 수 있는가?

NVLink가 있는 H100 SXM, A100 SXM, HGX/DGX류 서버라면 TP를 먼저 고려한다. TP는 레이어마다 GPU들이 결과를 합쳐야 하므로 빠른 인터커넥트가 매우 중요하다. NVLink가 있으면 이 통신 비용이 상대적으로 작아진다. 따라서 4GPU 또는 8GPU NVLink 서버에서는 다음과 같은 형태가 자연스럽다.

# 4GPU NVLink 서버
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 16384
# 8GPU NVLink 서버
vllm serve meta-llama/Llama-3.1-405B-Instruct \
  --tensor-parallel-size 8 \
  --dtype bfloat16 \
  --max-model-len 8192

반면 RTX 4090 여러 장, L40S PCIe 서버, 혹은 NVLink 없이 PCIe 스위치만 있는 구성이라면 다르게 생각해야 한다. TP는 PCIe 위에서도 동작하지만, 레이어마다 통신이 발생하기 때문에 기대보다 느릴 수 있다. 이때 모델이 균등하게 잘 쪼개지지 않거나, TP 크기가 attention head 수와 맞지 않거나, 통신 병목이 심하면 PP를 검토한다.

# 4GPU PCIe-only 서버에서 PP 우선 실험
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 1 \
  --pipeline-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 8192

혼합 전략도 가능하다. 예를 들어 8GPU 서버에서 GPU 4장씩 두 묶음으로 나누어, 묶음 내부는 TP, 묶음 사이에는 PP를 적용할 수 있다.

vllm serve meta-llama/Llama-3.1-405B-Instruct \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 2 \
  --dtype bfloat16 \
  --max-model-len 8192

결정 절차를 글로 정리하면 이렇다.

  1. 모델이 GPU 1장에 들어가고, 처리량도 충분하다면 멀티 GPU를 쓰지 않는다.

  2. 모델이 GPU 1장에는 안 들어가지만 NVLink가 있는 단일 노드에는 들어간다면 TP를 먼저 쓴다.

  3. NVLink가 없고 PCIe-only라면 TP=2 정도부터 실험하되, 통신 병목이 크면 PP를 비교한다.

  4. 모델 구조상 TP로 균등 분할이 어렵거나 GPU 메모리 크기가 섞여 있다면 PP를 우선 검토한다.

  5. 모델이 단일 노드 전체에도 안 들어가면 TP와 PP를 결합한다. 멀티 노드는 다음 장 이후의 주제지만, 기본 아이디어는 "노드 안에서는 TP, 노드 사이는 PP"다.

이 절차를 따르는 이유는 단순하다. TP는 빠른 대신 자주 통신하고, PP는 통신이 적은 대신 파이프라인 버블이 생긴다. NVLink 서버에서는 TP의 단점이 작고, PCIe-only 서버에서는 TP의 단점이 커진다.

VRAM을 결정하는 다섯 개의 손잡이

모델을 여러 GPU에 올렸다고 끝이 아니다. 오히려 이제부터가 진짜 vLLM 튜닝이다. vLLM 서버가 뜬 뒤 OOM 없이 얼마나 많은 요청을 받을 수 있는지는 다음 옵션들이 결정한다.

옵션조정 대상값을 키우면값을 줄이면
--gpu-memory-utilizationvLLM이 사용할 GPU 메모리 비율KV 캐시 공간 증가, OOM 위험 증가안정성 증가, 동시성 감소
--max-model-len요청 하나의 최대 컨텍스트 길이긴 문맥 가능, KV 캐시 부담 증가동시성 증가, 긴 요청 제한
--max-num-seqs동시에 스케줄링할 시퀀스 수처리량 증가 가능, 캐시 압박 증가안정성 증가, 처리량 제한
--kv-cache-dtypeKV 캐시 정밀도FP8 사용 시 캐시 절감품질·호환성 보수적
--max-num-batched-tokens한 배치의 총 토큰 예산TTFT/처리량 조정지연시간·메모리 균형 조정

gpu-memory-utilization: 1.0은 용기가 아니라 무모함이다

--gpu-memory-utilization은 vLLM이 GPU 메모리 중 어느 정도를 사용하도록 잡을지 정한다. 기본값은 보통 0.9다. 80GB GPU라면 대략 72GB를 vLLM이 사용 가능한 영역으로 보는 식이다. 나머지는 CUDA 런타임, 드라이버, PyTorch, vLLM 내부 오버헤드가 숨 쉴 공간이다.

--gpu-memory-utilization 0.90

OOM이 난다고 이 값을 무작정 0.98이나 1.0으로 올리면 안 된다. 가끔은 서버가 뜨는 것처럼 보이지만, 첫 요청이나 긴 요청에서 터진다. 운영에서는 0.88~0.92 정도로 시작하고, 전용 서버에서 안정성을 확인한 뒤 0.94~0.95까지 조심스럽게 올린다. 다른 프로세스가 같은 GPU를 쓰거나, 모니터링 에이전트·그래픽 세션·다른 모델 서버가 함께 있다면 0.80~0.88로 낮추는 편이 낫다.

vLLM 메모리 절약 문서도 큰 모델에서 OOM이 발생할 때 TP, 양자화, max_model_len, max_num_seqs 같은 옵션으로 메모리 사용량을 줄일 수 있다고 설명한다.2

max-model-len: 모델의 최대 컨텍스트를 그대로 믿지 말라

요즘 모델 카드에는 32K, 128K, 심지어 그 이상의 컨텍스트 길이가 적혀 있다. 그러나 그 숫자는 "모델이 구조적으로 처리할 수 있는 최대치"이지, "당신의 GPU 서버에서 그 길이로 여러 사용자를 동시에 처리할 수 있다"는 뜻이 아니다.

--max-model-len은 vLLM이 허용할 최대 시퀀스 길이다.

--max-model-len 8192

이 값이 클수록 긴 프롬프트를 받을 수 있지만, KV 캐시 예산도 커진다. RAG 서비스가 실제로는 문서 조각을 4K 안쪽으로 넣고, 답변을 1K 정도 생성한다면 128K 컨텍스트로 서버를 띄우는 것은 대부분 낭비다. 반대로 법률 문서 분석, 코드베이스 분석, 긴 회의록 요약처럼 진짜 긴 입력이 들어온다면 32K 이상이 필요할 수 있다. 이때는 GPU 수를 늘리거나 KV 캐시 dtype을 낮추는 식으로 대응해야 한다.

실전에서는 실제 요청 로그를 기준으로 잡는다. 대부분의 요청이 3,000토큰 이하이고 긴 요청도 6,000토큰 정도라면 8,192부터 시작한다. 12K~20K 요청이 자주 들어온다면 16,384 또는 32,768을 검토한다.

max-num-seqs: "동시 요청 수"처럼 보이지만 정확히 같지는 않다

--max-num-seqs는 한 번의 스케줄링 단계에서 동시에 다룰 수 있는 시퀀스 수의 상한이다.

--max-num-seqs 128

이 값이 크면 더 많은 요청을 한꺼번에 섞어 처리할 수 있어 총 처리량이 올라갈 수 있다. 하지만 모든 활성 시퀀스는 KV 캐시를 먹는다. 긴 프롬프트가 많은 서비스에서 이 값을 너무 크게 잡으면 preemption 경고가 쏟아지고, 응답 지연시간이 들쭉날쭉해진다.

작은 챗봇 서비스라면 32~128 정도로 시작한다. 70B 모델을 긴 컨텍스트로 돌린다면 64부터 보수적으로 시작해도 좋다. H100 8장처럼 VRAM 여유가 크고 처리량이 목표라면 256 이상도 실험할 수 있다.

kv-cache-dtype: 긴 컨텍스트의 숨은 구원투수

--kv-cache-dtype은 KV 캐시를 어떤 정밀도로 저장할지 정한다.

--kv-cache-dtype fp8

KV 캐시는 요청 길이와 동시성에 따라 커진다. FP16/BF16으로 저장하던 KV 캐시를 FP8로 줄이면 이론적으로 캐시 메모리 사용량을 크게 낮출 수 있다. 이 옵션은 긴 컨텍스트, 높은 동시성, 추론형 모델처럼 답변 토큰이 길어지는 워크로드에서 특히 효과가 크다.

다만 정밀도를 낮추는 선택에는 항상 검증이 필요하다. 일반 챗봇이나 RAG에서는 문제가 거의 보이지 않을 수 있지만, 수학·코딩·정밀 추론에서는 출력 품질을 비교해야 한다. 또 GPU 세대에 따라 FP8의 성능 이점과 호환성은 다르다. H100, H200, Blackwell 계열에서는 적극적으로 실험할 가치가 크고, A100에서는 메모리 절감 효과와 실제 연산 성능 이득을 구분해서 봐야 한다.

chunked prefill: 긴 프롬프트가 서버를 독점하지 않게 한다

프롬프트를 읽는 단계를 prefill이라고 한다. 긴 문서를 넣은 요청 하나가 들어오면 prefill이 오래 걸린다. 이때 짧은 채팅 요청들이 뒤에서 기다리면 사용자는 "서버가 멈췄다"고 느낀다.

chunked prefill은 긴 prefill을 작은 조각으로 나눠 decode 요청과 섞어 처리한다. vLLM V1에서는 가능한 경우 chunked prefill이 기본적으로 활성화되며, max_num_batched_tokens를 조정해 지연시간과 처리량 사이의 균형을 맞출 수 있다.3

--max-num-batched-tokens 8192

값을 작게 잡으면 진행 중인 decode 요청, 즉 이미 답변을 생성 중인 요청의 토큰 간 지연시간이 좋아질 수 있다. 대신 긴 프롬프트의 첫 토큰 시간, TTFT는 길어질 수 있다. 값을 크게 잡으면 prefill을 더 크게 처리할 수 있어 TTFT가 좋아질 수 있지만, decode 지연이 증가할 수 있다.

일반적인 출발점은 8192 또는 16384다. 짧은 채팅이 대부분이면 낮게, 긴 RAG 문서가 많고 첫 응답 시간을 줄이고 싶으면 높게 실험한다.

prefix caching: 같은 앞부분을 반복해서 읽지 않는다

서비스형 챗봇에는 흔히 긴 시스템 프롬프트가 있다. "너는 친절한 상담원이고, 다음 정책을 따라야 하며…" 같은 지시문이 모든 요청 앞에 붙는다. RAG 서비스에서는 같은 문서나 같은 지식 베이스 설명이 여러 요청에 반복되기도 한다.

--enable-prefix-caching은 이런 반복되는 prefix의 KV 캐시를 재사용하게 해준다.

--enable-prefix-caching

같은 접두사가 반복되는 워크로드에서는 TTFT가 눈에 띄게 좋아질 수 있다. 반대로 모든 요청의 앞부분이 완전히 다르면 효과가 작다. 멀티턴 대화, 고정 시스템 프롬프트, 같은 문서를 두고 여러 질문을 하는 RAG에서는 거의 항상 켜서 실험할 가치가 있다.

실전 설정 레시피: 2장, 4장, 8장 GPU

이제 옵션을 실제 명령으로 묶어 보자. 아래 예시는 정답이 아니라 출발점이다. 같은 70B 모델이라도 프롬프트 길이, 동시 요청, GPU 종류, 양자화 방식에 따라 최적값이 달라진다.

2× RTX 4090 또는 2× RTX 3090: 70B 양자화 모델 입문

소비자용 24GB GPU 두 장에서는 FP16 70B 모델은 어렵다. 보통 AWQ, GPTQ, FP8, INT4 계열의 양자화 체크포인트를 사용한다.

CUDA_VISIBLE_DEVICES=0,1 \
vllm serve TheBloke-or-org/your-70b-awq-model \
  --tensor-parallel-size 2 \
  --quantization awq \
  --dtype float16 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 64 \
  --enable-prefix-caching

PCIe-only라면 TP=2와 PP=2를 모두 시험해 본다.

CUDA_VISIBLE_DEVICES=0,1 \
vllm serve your-70b-quantized-model \
  --pipeline-parallel-size 2 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 64

두 설정을 비교할 때는 단일 요청 tokens/sec만 보지 말고, 동시 요청 8개, 16개, 32개에서 TTFT와 총 tokens/sec를 함께 봐야 한다. TP는 단일 요청이 더 빠를 수 있고, PP는 PCIe 병목을 덜 맞을 수 있다.

4× A100 80GB 또는 4× H100 80GB: 70B FP16/BF16 안정권

70B 모델을 BF16으로 운영하려면 4×80GB 구성이 훨씬 여유롭다. 모델 가중치가 GPU별로 나뉘고, KV 캐시 공간도 넉넉해진다.

CUDA_VISIBLE_DEVICES=0,1,2,3 \
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 128 \
  --max-num-batched-tokens 16384 \
  --enable-prefix-caching

H100 이상에서 KV 캐시를 더 줄이고 싶다면 다음을 추가해 실험한다.

--kv-cache-dtype fp8

긴 컨텍스트 RAG라면 --max-model-len 32768로 올려 볼 수 있지만, 이때는 로그에 나오는 GPU KV cache size와 maximum concurrency를 반드시 확인해야 한다. 긴 컨텍스트를 허용하는 순간, 동시 처리 가능 요청 수는 빠르게 줄어든다.

8× H100/H200: 70B 고처리량 또는 405B급 모델

8GPU 서버에서는 두 갈래가 있다. 70B 모델을 아주 높은 처리량으로 서빙하거나, 405B급 모델처럼 더 큰 모델을 올린다.

70B 고처리량 설정은 다음처럼 시작할 수 있다.

CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 8 \
  --dtype bfloat16 \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.92 \
  --max-num-seqs 256 \
  --max-num-batched-tokens 32768 \
  --kv-cache-dtype fp8 \
  --enable-prefix-caching

405B급 모델은 모델 정밀도와 GPU 메모리에 따라 훨씬 민감하다. FP8 또는 양자화 체크포인트를 쓰고, 처음에는 컨텍스트를 보수적으로 제한한다.

CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
vllm serve meta-llama/Llama-3.1-405B-FP8 \
  --tensor-parallel-size 8 \
  --dtype auto \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 32

만약 TP=8로 통신 병목이 크거나 모델 분할이 까다롭다면 TP=4, PP=2도 비교한다.

CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
vllm serve meta-llama/Llama-3.1-405B-FP8 \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 2 \
  --dtype auto \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90

vLLM 로그 읽기: GPU KV cache size와 maximum concurrency

vLLM을 띄우면 많은 로그가 지나간다. 초보자는 대부분 "서버가 떴나?"만 보지만, 운영자는 반드시 KV 캐시 관련 로그를 읽어야 한다. vLLM 공식 문서는 다음과 같은 로그를 예로 든다.1

INFO ... GPU KV cache size: 643,232 tokens
INFO ... Maximum concurrency for 40,960 tokens per request: 15.70x

첫 번째 줄은 GPU KV 캐시에 저장할 수 있는 총 토큰 수를 의미한다. 여기서는 전체 GPU 캐시가 643,232토큰을 담을 수 있다는 뜻이다.

두 번째 줄은 더 직관적이다. max_model_len이 40,960토큰일 때, 그 길이를 꽉 채운 요청을 약 15.7개 동시에 처리할 수 있다는 추정치다. 물론 실제 요청은 모두 40,960토큰을 꽉 채우지 않는다. 어떤 요청은 2,000토큰이고, 어떤 요청은 12,000토큰일 수 있다. 따라서 이 값은 절대 동시 사용자 수가 아니라 최악에 가까운 요청 길이를 기준으로 한 동시성 감각이다.

예를 들어 이런 로그가 있다고 하자.

GPU KV cache size: 200,000 tokens
Maximum concurrency for 8192 tokens per request: 24.41x

이 설정에서는 8K 요청 기준으로 약 24개를 동시에 감당할 수 있다고 볼 수 있다. 만약 서비스 목표가 "동시에 50명이 평균 4K 컨텍스트로 사용"이라면 어떻게 해석해야 할까? 평균 요청이 4K라면 단순 계산으로는 200,000 / 4,096 ≈ 48.8이다. 즉 거의 목표에 맞지만, 답변 생성 토큰과 요청 길이 편차를 고려하면 조금 빠듯하다.

이때 조정할 수 있는 방향은 몇 가지다. --max-model-len을 8192에서 6144로 낮추거나, --kv-cache-dtype fp8을 적용하거나, --gpu-memory-utilization을 0.90에서 0.92로 올리거나, GPU를 더 추가한다. 반대로 고객이 16K 문서를 자주 넣는다면 200,000 / 16,384 ≈ 12.2이므로, 50명 동시 처리는 불가능에 가깝다.

vLLM 로그는 서버의 현재 체력을 알려주는 건강검진표다. tokens/sec만 보고 "빠르다"고 판단하면 안 된다. GPU KV cache size가 작으면 실제 트래픽이 들어오는 순간 preemption과 지연시간 증가가 시작된다.

CUDA OOM이 날 때의 조정 순서

CUDA OOM은 대형 모델 서빙의 단골 손님이다. 하지만 OOM에도 종류가 있다. 모델 로딩 중에 터지는 OOM과 요청 처리 중에 터지는 OOM은 대응이 다르다.

모델 로딩 중 OOM

서버 시작 단계에서 모델이 올라가다 터진다면, 모델 가중치와 초기 버퍼가 GPU에 들어가지 않는 것이다. 이때 --gpu-memory-utilization을 올리는 것은 대개 도움이 안 된다. 오히려 런타임 여유 공간을 줄여 더 위험해진다.

조정 순서는 다음과 같다.

  1. --max-model-len을 낮춘다. 모델 로딩 직후 vLLM은 KV 캐시 프로파일링과 예약을 수행하므로, 너무 큰 컨텍스트는 시작 단계 OOM을 만들 수 있다.

  2. --dtype을 확인한다. A100 이상에서는 BF16이 안정적이고, H100 이상에서는 FP8 모델 또는 FP8 KV 캐시를 검토할 수 있다.

  3. 양자화 모델을 사용한다. AWQ, GPTQ 등 vLLM이 지원하는 형식의 체크포인트를 검토한다.

  4. --tensor-parallel-size 또는 --pipeline-parallel-size를 늘려 모델 가중치를 더 많은 GPU에 나눈다.

  5. 그래도 안 되면 더 작은 모델을 선택한다. "옵션으로 해결할 수 없는 물리적 한계"도 있다.

요청 처리 중 OOM

서버는 떴는데 긴 요청이나 동시 요청이 들어오면서 터진다면, 대부분 KV 캐시가 문제다. 이때는 다음 순서로 조정한다.

  1. --max-model-len을 실제 사용량에 맞게 낮춘다.

  2. --max-num-seqs를 낮춰 동시에 처리하는 시퀀스 수를 줄인다.

  3. --kv-cache-dtype fp8을 실험한다.

  4. --gpu-memory-utilization을 0.90에서 0.92 또는 0.94로 조심스럽게 올린다.

  5. --tensor-parallel-size나 --pipeline-parallel-size를 늘려 GPU당 가중치 부담을 줄이고 KV 캐시 공간을 확보한다.

vLLM의 메모리 절약 문서에서도 context length와 batch size를 제한하는 것이 메모리 사용량을 줄이는 중요한 방법이라고 설명한다.2

NCCL 오류가 날 때의 조정 순서

멀티 GPU에서 NCCL 오류는 피할 수 없는 통과의례처럼 느껴질 때가 있다. NCCL은 GPU 간 통신을 맡기 때문에, TP를 쓰는 순간 중요해진다.

가장 먼저 확인할 것은 GPU가 모두 보이는지다.

nvidia-smi

다음으로 vLLM에 보이는 GPU 수와 병렬 옵션의 곱이 맞는지 확인한다.

보이는 GPU 수 = TP × PP × DP

4장만 보이는데 TP=8로 실행하면 당연히 실패한다. CUDA_VISIBLE_DEVICES를 잘못 지정했거나 Docker에서 --gpus all을 빼먹은 경우도 흔하다.

그다음은 토폴로지다.

nvidia-smi topo -m

이 명령은 GPU들이 NVLink로 연결되어 있는지, PCIe 스위치를 공유하는지, CPU 소켓을 건너 통신하는지 보여준다. 멀티 소켓 서버에서 GPU와 CPU NUMA 배치가 꼬이면 성능이 크게 떨어질 수 있다. vLLM은 GPU worker 프로세스를 가까운 NUMA 노드에 묶는 --numa-bind 옵션도 제공한다.3

vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --numa-bind

NCCL 오류가 반복되면 다음 순서로 줄여 간다.

  1. TP 크기를 낮춘다. 예를 들어 TP=8에서 TP=4 또는 TP=2로 줄여 통신 복잡도를 낮춘다.

  2. PCIe-only 서버라면 PP 구성을 비교한다.

  3. Docker라면 --ipc=host, 충분한 --shm-size, GPU 노출 설정을 확인한다.

  4. 드라이버, CUDA, PyTorch, vLLM 버전 조합을 맞춘다.

  5. 멀티 노드가 아니라 단일 노드라면 Ray 대신 기본 multiprocessing 경로로 단순화한다.

단일 노드에서 불필요하게 Ray를 끼우면 문제 원인이 늘어난다. vLLM은 단일 노드에서는 기본적으로 Python multiprocessing을 사용할 수 있고, 멀티 노드에서는 Ray를 주로 사용한다.1

preemption 경고가 뜰 때의 조정 순서

vLLM을 운영하다 보면 이런 경고를 볼 수 있다.

Sequence group is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.

이 메시지는 KV 캐시 공간이 부족해서 진행 중인 요청 일부를 잠시 밀어내고, 나중에 다시 계산한다는 뜻이다. vLLM 최적화 문서는 이 메커니즘이 시스템을 견고하게 만들지만, 재계산 때문에 end-to-end latency가 나빠질 수 있다고 설명한다.3

가끔 한두 번 뜨는 것은 큰 문제가 아닐 수 있다. 트래픽이 순간적으로 몰렸을 때 vLLM이 버티는 방식이기도 하다. 그러나 지속적으로 뜬다면 응답 시간이 흔들리고 있다는 신호다.

조정 순서는 다음과 같다.

  1. --gpu-memory-utilization을 조금 올린다. 예를 들어 0.90에서 0.92로 올려 KV 캐시 풀을 늘린다.

  2. --max-num-seqs를 줄인다. 너무 많은 요청을 한 번에 잡으려다 캐시가 부족한 상황을 완화한다.

  3. --max-num-batched-tokens를 조정한다. 긴 prefill이 많은 경우 배치 토큰 예산을 재조정한다.

  4. --max-model-len을 낮춘다. 실제로 8K만 필요한 서비스가 32K로 떠 있다면 캐시 예산을 낭비하고 있는 것이다.

  5. GPU를 더 쓰거나 TP/PP를 늘린다. 모델 가중치가 더 잘 분산되면 GPU당 KV 캐시 여유가 늘 수 있다.

중요한 것은 preemption을 "에러"로만 보지 않는 것이다. preemption은 vLLM이 죽지 않고 버티고 있다는 뜻이기도 하다. 하지만 운영 품질 관점에서는 자주 발생하지 않도록 조정해야 한다.

tokens/sec가 기대보다 낮을 때

서버는 떴고 OOM도 없는데, tokens/sec가 기대보다 낮을 수 있다. 이때는 원인을 네 갈래로 나누어 본다.

첫째, 통신 병목이다. TP를 크게 잡았는데 PCIe-only 서버라면 GPU들이 계산보다 기다리는 시간이 많을 수 있다. nvidia-smi dmon, DCGM, Prometheus exporter 등으로 GPU utilization과 PCIe/NVLink 트래픽을 함께 본다. GPU 사용률이 낮고 통신이 높다면 TP를 줄이거나 PP를 비교한다.

둘째, 배치가 충분하지 않다. vLLM은 연속 배칭에서 힘을 발휘한다. 단일 요청 하나만 던지고 "왜 GPU 8장인데 8배 빠르지 않지?"라고 묻는 것은 공정한 실험이 아니다. 동시 요청을 늘려 총 처리량을 봐야 한다.

셋째, prefill과 decode의 균형이 맞지 않는다. 긴 프롬프트가 많으면 TTFT가 느려진다. 이때 --max-num-batched-tokens와 chunked prefill 관련 설정을 조정한다. vLLM V1의 chunked prefill은 decode를 우선하면서 남는 토큰 예산에 prefill을 넣는 방식으로 처리량과 지연시간 균형을 맞춘다.3

넷째, prefix caching 기회를 놓치고 있다. 고정 시스템 프롬프트나 반복 문서가 많다면 --enable-prefix-caching을 켜고 TTFT 변화를 측정한다.

운영용 기본 명령 템플릿

마지막으로 단일 노드 멀티 GPU 운영에서 복사해 시작할 수 있는 템플릿을 정리해 보자. 먼저 NVLink가 있는 4GPU 서버에서 70B 모델을 BF16으로 서빙하는 예다.

export HF_TOKEN=hf_xxxxxxxxxxxxxxxxx
export CUDA_VISIBLE_DEVICES=0,1,2,3

vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 128 \
  --max-num-batched-tokens 16384 \
  --enable-prefix-caching

H100 이상에서 긴 컨텍스트와 동시성을 더 확보하고 싶다면 다음처럼 바꾼다.

export CUDA_VISIBLE_DEVICES=0,1,2,3

vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --dtype bfloat16 \
  --kv-cache-dtype fp8 \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.92 \
  --max-num-seqs 128 \
  --max-num-batched-tokens 32768 \
  --enable-prefix-caching

PCIe-only 4GPU 서버에서 PP를 비교하고 싶다면 다음처럼 실행한다.

export CUDA_VISIBLE_DEVICES=0,1,2,3

vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 1 \
  --pipeline-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 64 \
  --enable-prefix-caching

Docker 운영 템플릿은 다음처럼 잡을 수 있다.

docker run --rm --gpus all \
  --ipc=host \
  --network host \
  -e HF_TOKEN=$HF_TOKEN \
  -e CUDA_VISIBLE_DEVICES=0,1,2,3 \
  vllm/vllm-openai:latest \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --dtype bfloat16 \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.90 \
  --max-num-seqs 128 \
  --max-num-batched-tokens 16384 \
  --enable-prefix-caching

이 템플릿을 그대로 운영에 넣기보다는, 반드시 로그의 GPU KV cache size와 Maximum concurrency를 확인하고 부하 테스트를 거쳐야 한다. 평균 요청 길이, 95퍼센타일 프롬프트 길이, 평균 출력 토큰 수, 동시 요청 수를 넣어 봐야 진짜 설정이 나온다.

vLLM 튜닝의 핵심은 "얼마나 큰 모델인가"가 아니라 "어떤 요청이 들어오는가"다

많은 사람이 vLLM 설정을 모델 크기에서 시작한다. "70B니까 GPU 몇 장이면 되나요?" 물론 중요한 질문이다. 하지만 운영에서는 그다음 질문이 더 중요하다.

같은 70B 모델이라도 2K 채팅 요청 100개를 받는 서버와, 32K RAG 요청 10개를 받는 서버는 완전히 다른 시스템이다. 전자는 배칭과 decode 처리량이 중요하고, 후자는 prefill과 KV 캐시가 중요하다. 같은 4GPU 서버라도 NVLink가 있으면 TP가 자연스럽고, PCIe-only면 PP가 더 나을 수 있다. 같은 H100 서버라도 FP8 KV 캐시를 켜면 긴 컨텍스트 동시성이 달라진다.

vLLM을 잘 쓴다는 것은 옵션을 많이 아는 것이 아니다. 로그를 읽고, 병목을 추정하고, 한 번에 하나씩 바꾸는 것이다. CUDA OOM이 나면 max-model-len과 병렬 크기를 본다. NCCL 오류가 나면 GPU 수와 토폴로지를 본다. preemption이 뜨면 KV 캐시 압박을 본다. tokens/sec가 낮으면 통신, 배치, prefill, prefix 재사용을 차례로 본다.

이제 우리는 단일 노드 멀티 GPU 서버에서 vLLM으로 대형 모델을 올리는 기본기를 갖췄다. 다음 장에서는 이 기본기를 MoE 모델로 확장한다. MoE는 겉으로는 "활성 파라미터가 작아 효율적인 모델"처럼 보이지만, 실제 운영에서는 전문가 배치, 라우팅, KV 캐시, 병렬 전략이 복잡하게 얽힌다. 단일 노드 vLLM을 이해했다면, 이제 전문가 병렬이라는 더 흥미로운 세계로 들어갈 준비가 된 것이다.

참고

1vLLM Parallelism and Scaling

2vLLM Conserving Memory

3vLLM Optimization and Tuning

책 목록

VRAM의 벽을 넘는 멀티 GPU LLM 운영 가이드

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기