본문으로 건너뛰기
AIPida

vLLM로 LLM 자체 호스팅

PagedAttention 기반 추론 서버를 직접 띄우고, OOM을 피하면서 처리량을 끝까지 짜내는 실전 가이드

실전AI 개발·

OpenAI API에 의존하던 워크로드를 자체 GPU로 옮기는 순간, 가장 먼저 만나는 도구가 vLLM이다. vLLM은 UC 버클리에서 출발한 오픈소스 추론 엔진으로, PagedAttention(KV 캐시를 OS 가상 메모리처럼 페이지 단위로 관리)과 continuous batching(요청이 끝나는 즉시 빈 슬롯에 새 요청을 끼워 넣는 동적 배칭)을 핵심으로 한다. 이 두 가지 덕분에 naive한 HuggingFace pipeline 대비 같은 GPU에서 수 배에서 수십 배의 처리량을 낸다. KV 캐시 단편화를 4% 미만으로 줄이는 게 핵심 원리다.

자체 호스팅을 택하는 이유는 보통 셋 중 하나다. (1) 비용 — 트래픽이 일정 규모를 넘으면 토큰당 API 과금보다 GPU를 상시 점유하는 쪽이 싸진다. (2) 데이터 주권 — 프롬프트와 출력이 외부로 나가면 안 되는 도메인(의료·금융·사내 코드). (3) 레이턴시·커스터마이징 — 파인튜닝한 모델, LoRA 어댑터, 특정 양자화 포맷을 그대로 서빙해야 할 때. 그런데 막상 띄워 보면 vllm serve 한 줄로 끝나는 게 아니라, GPU OOM, KV 캐시 부족, 낮은 GPU 활용률 같은 벽에 부딪힌다. 이 가이드는 그 벽들을 순서대로 넘는다.

전체 흐름은 이렇게 잡는다.

단계내용
1. 설치CUDA 호환 pip 설치 또는 공식 Docker 이미지
2. 첫 서버vllm serve로 OpenAI 호환 엔드포인트 띄우기
3. 호출OpenAI SDK / curl로 chat·completions 호출
4. 메모리 모델 이해가중치 + KV 캐시 = VRAM 예산
5. OOM·KV 캐시 디버깅max-model-len·gpu-memory-utilization 조정
6. 처리량 튜닝max-num-seqs·max-num-batched-tokens·chunked prefill
7. 멀티 GPUtensor / pipeline parallelism
8. 양자화AWQ·GPTQ·FP8로 큰 모델 욱여넣기
9. 프로덕션화벤치마크·관측·게이트 모델·보안

이 가이드의 모든 수치는 "개념적 출발점"이지 마법의 상수가 아니다. 반드시 본인 모델·GPU·트래픽 패턴으로 벤치마크해서 검증하라. 마지막 섹션에 vLLM이 기본 제공하는 벤치마크 도구를 다룬다.

1. 설치 — pip vs Docker, 그리고 CUDA 지옥 피하기

vLLM은 NVIDIA GPU(CUDA)에서 가장 성숙하고, AMD ROCm·Intel·CPU·Apple Silicon(별도 포크)도 지원한다. 이 가이드는 가장 일반적인 NVIDIA CUDA 환경을 기준으로 한다.

방법 A: pip 설치 (개발·실험용)

vLLM은 자기 자신과 호환되는 PyTorch·CUDA 런타임을 휠 안에 묶어 배포한다. 가상환경을 새로 파고 설치하는 게 안전하다.

# Python 3.10~3.12 권장. 새 가상환경에서.
python -m venv .venv && source .venv/bin/activate
pip install --upgrade pip

# 기본 설치 (기본 CUDA 빌드)
pip install vllm

드라이버가 특정 CUDA 버전에 묶여 있으면, PyTorch 인덱스를 지정해 맞는 빌드를 받는다. (CUDA 버전 숫자는 본인 nvidia-smi 출력에 맞춰라 — 아래는 예시일 뿐이다.)

pip install vllm --extra-index-url https://download.pytorch.org/whl/cu121

오디오·멀티모달 등 추가 의존성이 필요하면 extras로 설치한다.

pip install "vllm[audio]"

방법 B: 공식 Docker 이미지 (프로덕션 권장)

로컬 CUDA·드라이버 버전 충돌을 가장 확실하게 피하는 길이다. 호스트에 NVIDIA 드라이버와 NVIDIA Container Toolkit만 깔려 있으면 된다.

docker run --runtime nvidia --gpus all \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --env "HF_TOKEN=$HF_TOKEN" \
  -p 8000:8000 \
  --ipc=host \
  vllm/vllm-openai:latest \
  --model Qwen/Qwen2.5-7B-Instruct

각 플래그의 의미:

  • --gpus all — 호스트 GPU를 컨테이너에 노출
  • -v ~/.cache/huggingface:... — 모델 가중치를 호스트에 캐싱해 재시작 시 재다운로드 방지
  • --ipc=host — 텐서 병렬에서 vLLM이 쓰는 공유 메모리(/dev/shm)를 호스트와 공유. 이게 없으면 멀티 GPU에서 무음 hang이 난다. 대안으로 --shm-size=10g
  • :latest 대신 :v0.x.y처럼 태그를 고정하라 — vLLM은 마이너 릴리스마다 기본값·플래그가 자주 바뀐다

흔한 함정

  • 시스템 Python에 바로 설치 → 기존 torch와 ABI 충돌로 undefined symbol 에러. 항상 깨끗한 venv나 컨테이너에서.
  • 드라이버가 휠의 CUDA보다 낮음CUDA error: no kernel image is available. 드라이버를 올리거나 맞는 휠을 받아라.
  • GPU 아키텍처 미지원 — 아주 오래된 GPU(컴퓨트 캐퍼빌리티가 낮은 카드)는 미리 빌드된 휠로 안 돌 수 있다. 소스 빌드가 필요할 수 있다.

베스트 프랙티스

프로덕션은 Docker + 태그 고정 + HF 캐시 볼륨 마운트를 기본형으로 잡아라. 재현성이 압도적으로 좋다. 설치 직후 python -c "import vllm; print(vllm.__version__)"로 버전을 박제해 두면, 나중에 "왜 갑자기 기본값이 달라졌지" 디버깅이 쉬워진다.

2. 첫 서버 띄우기 — vllm serve와 OpenAI 호환 엔드포인트

vLLM의 핵심 진입점은 vllm serve다. 이 명령은 OpenAI 호환 HTTP 서버를 띄운다. 즉 /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/models 같은 엔드포인트를 그대로 노출하므로, 기존에 OpenAI를 쓰던 코드는 base URL만 바꾸면 거의 그대로 동작한다.

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --host 0.0.0.0 \
  --port 8000

처음 실행하면 vLLM이 HuggingFace에서 가중치를 받고, 모델을 GPU에 올리고, KV 캐시를 프로파일링해 사전 할당한 뒤, 마지막에 Application startup complete 비슷한 로그와 함께 listen을 시작한다. 이 startup 단계가 끝나기 전에 요청을 보내면 연결 거부가 난다.

상태 확인

# 떠 있는 모델 목록 — served-model-name이 그대로 model id가 된다
curl http://localhost:8000/v1/models

# 헬스 체크 (200이면 OK)
curl http://localhost:8000/health

모델 이름 별칭 지정

기본적으로 API에서 쓰는 model 값은 HuggingFace 경로 전체(Qwen/Qwen2.5-7B-Instruct)다. 클라이언트 코드를 깔끔하게 하려면 별칭을 준다.

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --served-model-name qwen-7b \
  --port 8000
# 이제 클라이언트는 model="qwen-7b"로 호출

핵심 startup 플래그 미리 보기

실전에서 거의 항상 같이 쓰는 플래그들이다. 각각은 뒤 섹션에서 깊게 다룬다.

vllm serve <model> \
  --host 0.0.0.0 --port 8000 \
  --api-key sk-내부키 \              # 간단한 인증
  --max-model-len 8192 \             # 컨텍스트 상한 (KV 캐시 예산 직결)
  --gpu-memory-utilization 0.90 \    # VRAM 중 vLLM이 점유할 비율
  --max-num-seqs 256                 # 동시 처리 시퀀스 상한

흔한 함정

  • --host를 빼먹음 → 기본이 localhost라 컨테이너/원격에서 접근 불가. 외부 노출이 필요하면 0.0.0.0 (단, 반드시 인증/방화벽과 함께).
  • chat 템플릿 미적용 — base 모델(instruct 아님)은 chat 템플릿이 없어서 /v1/chat/completions가 엉뚱하게 동작한다. instruct/chat 튜닝된 모델을 쓰거나 --chat-template 파일을 직접 지정하라.
  • startup이 오래 걸림 — 70B급은 가중치 다운로드+로딩에 수 분이 걸린다. 헬스체크 타임아웃을 넉넉히.

베스트 프랙티스

--served-model-name으로 안정적인 별칭을 박아두면, 나중에 백엔드 모델을 교체해도 클라이언트 코드를 안 건드린다. 그리고 startup 로그에서 vLLM이 출력하는 "# GPU blocks / # KV cache tokens" 줄을 꼭 읽어라 — 이 숫자가 당신이 실제로 동시에 처리할 수 있는 토큰 총량이고, 처리량 튜닝의 출발점이다.

3. 클라이언트에서 호출하기 — OpenAI SDK·curl·스트리밍

서버가 OpenAI 호환이므로, 공식 OpenAI Python/JS SDK를 그대로 쓰되 base_urlapi_key만 바꾼다.

Python (OpenAI SDK)

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="sk-내부키",  # --api-key로 지정한 값. 안 걸었으면 아무 문자열
)

resp = client.chat.completions.create(
    model="qwen-7b",  # served-model-name
    messages=[
        {"role": "system", "content": "너는 간결한 한국어 어시스턴트다."},
        {"role": "user", "content": "PagedAttention을 한 문장으로 설명해줘."},
    ],
    temperature=0.7,
    max_tokens=256,
)
print(resp.choices[0].message.content)

스트리밍 (토큰 단위 응답)

사용자 체감 레이턴시(첫 토큰까지의 시간, TTFT)를 줄이려면 스트리밍이 거의 필수다.

stream = client.chat.completions.create(
    model="qwen-7b",
    messages=[{"role": "user", "content": "긴 글을 써줘"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

curl (디버깅·헬스체크용)

curl http://localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer sk-내부키" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen-7b",
    "messages": [{"role": "user", "content": "안녕"}],
    "max_tokens": 64
  }'

vLLM 고유 파라미터 (sampling extras)

vLLM은 OpenAI 스펙에 없는 샘플링 파라미터를 extra_body로 받는다. 예: top_k, repetition_penalty, guided_json(구조화 출력).

resp = client.chat.completions.create(
    model="qwen-7b",
    messages=[{"role": "user", "content": "JSON으로 사용자 정보를 만들어"}],
    extra_body={
        "top_k": 50,
        "repetition_penalty": 1.05,
        "guided_json": {
            "type": "object",
            "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
            "required": ["name", "age"],
        },
    },
)

guided_json/guided_regex/guided_choice는 vLLM이 디코딩 단계에서 문법을 강제해 항상 유효한 구조화 출력을 보장한다. 후처리 파싱 에러를 거의 없앤다.

흔한 함정

  • api_key를 빈 문자열로 — OpenAI SDK는 빈 키를 거부한다. 서버에 인증을 안 걸었어도 클라이언트엔 아무 더미 문자열을 넣어라.
  • max_tokens를 안 줌 — 모델이 EOS를 안 뱉으면 컨텍스트 끝까지 생성해서 한 요청이 KV 캐시를 다 먹는다. 항상 상한을 둬라.
  • 타임아웃 — 큰 배치/긴 출력은 기본 HTTP 타임아웃을 넘긴다. 클라이언트 타임아웃을 늘리고 스트리밍을 켜라.

4. VRAM 예산 이해하기 — 가중치 + KV 캐시 = 전부

처리량 튜닝과 OOM 디버깅의 모든 것은 GPU 메모리가 어디에 쓰이는지 이해하는 데서 시작한다. vLLM이 GPU에 잡는 메모리는 크게 셋이다.

[ 전체 VRAM ]
 ├── 모델 가중치        (모델 크기 × 정밀도. 고정)
 ├── 활성화/CUDA 그래프  (작업 버퍼. 비교적 작음)
 └── KV 캐시 풀          (★ 남은 공간 전부 — 동시성을 좌우)

가중치 크기 어림셈

파라미터 수 × 바이트/파라미터다.

정밀도바이트/파라미터7B 모델70B 모델
FP16/BF162~14GB~140GB
FP81~7GB~70GB
INT4 (AWQ/GPTQ)0.5~3.5GB~35GB

그래서 70B FP16은 A100/H100 80GB 한 장에 안 들어간다(가중치만 140GB → GPU 2장 이상 필요). 반면 INT4로 양자화하면 70B가 35GB로 줄어 단일 80GB 카드에 가중치가 들어가고, 남는 45GB를 KV 캐시로 쓸 수 있다.

KV 캐시가 동시성을 결정한다

KV 캐시는 생성 중인 각 시퀀스의 어텐션 key/value를 저장한다. 토큰 하나당 일정 메모리를 먹고, 이는 레이어 수·헤드 수·차원·정밀도에 비례한다. 핵심은:

가중치를 올리고 남은 VRAM이 곧 KV 캐시 풀이고, 그 풀이 클수록 동시에 더 많은 요청·더 긴 컨텍스트를 처리할 수 있다.

vLLM은 startup 시 이 풀을 프로파일링해서 "GPU blocks: N" 같은 로그를 찍는다. 블록 하나는 보통 16토큰 단위다. 이 숫자가 작으면 동시 요청이 몇 개만 들어와도 나머지는 큐에서 대기(또는 preempt)한다.

--gpu-memory-utilization

이 플래그는 "GPU의 몇 %까지 vLLM이 점유할지"다. 기본은 0.9 근처. 가중치를 올린 뒤 이 비율까지 남는 만큼을 전부 KV 캐시로 잡는다.

# GPU를 독점하는 베어메탈/전용 인스턴스: 공격적으로
--gpu-memory-utilization 0.95

# 같은 GPU에 다른 프로세스(임베딩 모델 등)가 있으면: 보수적으로
--gpu-memory-utilization 0.80

흔한 함정

  • 0.95에서 런타임 OOM — startup은 통과했는데 트래픽이 몰리면 OOM. 프로파일링이 예측 못한 활성화 피크 때문이다. 0.90으로 내려 여유를 둬라.
  • 같은 카드에 두 모델 — 두 vLLM 인스턴스를 한 GPU에 띄우면서 둘 다 0.9를 쓰면 합이 1.8 > 1.0이라 두 번째가 "No available memory for the cache blocks"로 죽는다. 합이 1.0 미만이 되게 나눠라.

베스트 프랙티스

startup 로그의 KV 캐시 토큰 수를 기록해 두고, (KV 캐시 토큰) ÷ (요청당 평균 토큰) ≈ 실질 동시성으로 어림하라. 이 숫자가 목표 동시성보다 작으면, 양자화로 가중치를 줄이거나 max-model-len을 낮춰 KV 풀을 키워야 한다.

5. OOM과 KV 캐시 에러 디버깅 — 가장 자주 막히는 지점

자체 호스팅에서 90%의 좌절은 두 에러로 수렴한다. 증상과 처방을 정리한다.

증상 A: startup에서 KV 캐시 부족

ValueError: To serve at least one request with the model's max seq len (32768),
(X GB KV cache is needed, which is larger than the available KV cache memory (Y GB)).
Try increasing gpu_memory_utilization or decreasing max_model_len.

원인: vLLM은 단 한 개의 요청이라도 max-model-len 길이까지 받을 수 있어야 부팅한다. 모델의 기본 컨텍스트(예: 32K)가 너무 길어서 KV 캐시가 그만큼을 못 잡는 것.

처방 (효과 큰 순):

# 1) max-model-len을 실제 필요한 만큼으로 낮춤 — KV 캐시 요구량이 선형으로 줄어든다
#    32768 → 8192면 KV 캐시 요구가 약 4배 감소
--max-model-len 8192

# 2) gpu-memory-utilization을 올려 KV 풀 확대
--gpu-memory-utilization 0.92

# 3) 가중치를 줄이는 양자화 모델로 교체 (8섹션)

증상 B: 런타임 CUDA OOM

torch.OutOfMemoryError: CUDA out of memory.

startup은 됐는데 트래픽 중 죽는 경우. 처방:

# 동시 시퀀스 상한을 낮춰 피크 KV 수요 억제
--max-num-seqs 128

# CUDA 그래프 캡처 메모리를 아껴 1.5~2GB 회수 (단, 약간 느려짐)
--enforce-eager

# utilization을 살짝 낮춰 안전 마진
--gpu-memory-utilization 0.88

--enforce-eager 트레이드오프

vLLM은 기본적으로 CUDA 그래프를 캡처해 커널 런치 오버헤드를 줄이는데, 이게 추가 VRAM을 먹는다. --enforce-eager는 그래프 캡처를 끄고 메모리를 회수하지만 약간의 속도 손해가 있다. 메모리가 빠듯한 24GB급 카드에서 모델을 일단 띄우는 데 유용하다.

진단 체크리스트

확인명령/방법
실제 가용 VRAMnvidia-smi (다른 프로세스가 먹고 있나?)
vLLM이 잡은 KV 토큰 수startup 로그 "KV cache" 줄
max-model-len이 과한가모델 카드의 실제 필요 컨텍스트와 비교
다른 프로세스 점유좀비 파이썬 프로세스 kill

흔한 함정

  • "VRAM은 충분한데 OOM"nvidia-smi에 다른(좀비) 프로세스가 메모리를 잡고 있는 경우가 흔하다. vLLM이 보는 "가용" 메모리는 시작 시점 잔여량이다.
  • max-model-len을 무작정 크게 — 32K로 띄우면 동시성이 급락한다. 실제로 4K 컨텍스트만 쓰는 서비스인데 32K로 잡으면 KV 풀을 8배 낭비한다.
  • --enforce-eager를 프로덕션 기본값으로 — 메모리 응급 처치엔 좋지만 처리량이 떨어진다. 메모리에 여유가 생기면 다시 꺼서 CUDA 그래프를 켜라.

베스트 프랙티스

max-model-len은 "모델이 지원하는 최대"가 아니라 "서비스가 실제로 필요한 최대"로 잡아라. 이 한 가지가 OOM 회피와 동시성 확보에 가장 큰 레버다.

6. 처리량 튜닝 — max-num-seqs, max-num-batched-tokens, chunked prefill

메모리 문제를 넘겼다면, 이제 GPU를 놀리지 않고 처리량(throughput)을 끌어올릴 차례다. 핵심은 continuous batching의 배치를 충분히 크게, 그러나 레이턴시를 안 죽일 만큼만 채우는 것이다.

두 개의 핵심 다이얼

--max-num-seqs 256             # 한 배치에 동시에 들어갈 수 있는 시퀀스(요청) 수
--max-num-batched-tokens 8192  # 한 스텝에서 처리할 토큰 총 예산
  • --max-num-seqs — 동시성 상한. 높이면 더 많은 요청을 in-flight로 들고 가지만 KV 캐시를 더 먹는다. 트래픽 피크 동시성에 맞춘다(예: 고트래픽 API 256~512).
  • --max-num-batched-tokens — 한 스텝의 토큰 처리량 예산. 높이면 처리량↑, time-to-first-token(TTFT)↑. 낮으면 토큰 간 레이턴시(ITL)는 좋아지지만 처리량↓.

⚠️ 둘은 같이 움직여야 한다. max-num-seqs만 올리고 max-num-batched-tokens를 안 올리면, 스케줄러가 토큰 예산에 막혀 시퀀스 슬롯을 못 채운다. 동시성을 올렸으면 토큰 예산도 함께 올려라.

Chunked Prefill — prefill과 decode를 한 배치에

vLLM V1 엔진(현 기본)은 chunked prefill을 기본 활성화한다. 긴 프롬프트의 prefill(입력 인코딩)을 작은 청크로 쪼개서 decode(토큰 생성) 요청과 같은 배치에 섞는다. 효과:

  • 긴 프롬프트 하나가 들어와도 다른 사용자의 토큰 생성이 멈추지 않는다(head-of-line blocking 완화)
  • decode를 우선해 ITL과 생성 속도가 좋아진다

토큰 예산이 곧 청크 크기 상한 역할을 한다.

워크로드별 출발점

워크로드목표권장 방향
오프라인 배치(요약·임베딩 대량)최대 처리량max-num-seqs 크게(512), max-num-batched-tokens 크게(8192~16384)
실시간 챗(대화형)낮은 TTFT/ITLmax-num-batched-tokens 중간, 스트리밍 ON
긴 컨텍스트 RAG균형chunked prefill 활용, max-model-len은 실측 필요만큼

Prefix Caching — 반복 프롬프트 재활용

시스템 프롬프트나 공통 문서 프리픽스가 매 요청 반복된다면, prefix caching이 이미 계산한 KV를 재사용해 prefill 비용을 크게 줄인다.

--enable-prefix-caching

같은 시스템 프롬프트를 쓰는 멀티턴 챗, 동일 컨텍스트로 여러 질문을 던지는 RAG에서 효과가 크다.

흔한 함정

  • 레이턴시만 보고 max-num-seqs를 너무 낮춤 — 동시성이 막혀 GPU가 놀고 처리량이 바닥난다. 단건 레이턴시와 집계 처리량은 트레이드오프다.
  • max-num-batched-tokens < max-model-len (chunked prefill 끈 경우) — 긴 요청이 한 배치에 안 들어가 에러. chunked prefill이 켜져 있으면 자동 청킹되지만, 끈 상태라면 예산이 컨텍스트보다 커야 한다.
  • 튜닝을 감으로 — 반드시 부하 테스트(다음 섹션 벤치마크)로 P50/P99 레이턴시와 토큰/초를 측정하며 조정하라.

베스트 프랙티스

튜닝은 한 번에 한 다이얼씩 바꾸고 벤치마크로 효과를 측정하라. 보통 순서는 (1) gpu-memory-utilization을 안전 최대치로 → (2) max-num-seqs를 목표 동시성까지 → (3) max-num-batched-tokens로 처리량/레이턴시 균형 잡기 → (4) prefix caching 켜기.

7. 멀티 GPU 서빙 — Tensor Parallel과 Pipeline Parallel

모델이 GPU 한 장에 안 들어가거나(70B FP16), 처리량을 더 키워야 할 때 여러 GPU로 분산한다. vLLM은 두 가지 병렬화를 제공한다.

Tensor Parallelism (TP) — 레이어를 가로로 쪼갬

각 레이어의 가중치를 여러 GPU에 나눠 싣고, 모든 GPU가 매 레이어를 협력해 계산한다. GPU 간 all-reduce/all-gather 통신이 매 레이어 발생하므로, **GPU 간 고속 인터커넥트(NVLink)**가 있을 때 효율이 좋다.

# 한 노드에 GPU 4장으로 70B 모델 분산
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --gpu-memory-utilization 0.90

vLLM이 GPU 간 통신을 전부 자동으로 처리한다. tensor-parallel-size는 모델의 어텐션 헤드 수의 약수여야 한다(보통 2의 거듭제곱: 2, 4, 8).

Pipeline Parallelism (PP) — 레이어를 세로로 쪼갬

모델의 레이어 블록을 GPU에 순차 배치한다(GPU0=레이어 0~19, GPU1=20~39 …). 통신량이 TP보다 적어 NVLink 없이 PCIe·여러 노드로 묶을 때 유리하다.

--pipeline-parallel-size 2

언제 무엇을

상황선택
한 노드, NVLink 연결된 GPU들TP 우선
노드 간(여러 머신) 분산PP(또는 TP×PP 조합)
TP 효율이 더 안 오를 때 추가 분산TP × PP 결합

매우 큰 모델은 둘을 곱해서 쓴다(예: --tensor-parallel-size 8 --pipeline-parallel-size 2 = GPU 16장).

Docker에서 멀티 GPU

텐서 병렬은 공유 메모리를 쓰므로 컨테이너에 **반드시 --ipc=host(또는 충분한 --shm-size)**가 필요하다. 빠뜨리면 무음 hang이 난다.

docker run --runtime nvidia --gpus all --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --env "HF_TOKEN=$HF_TOKEN" -p 8000:8000 \
  vllm/vllm-openai:latest \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4

흔한 함정

  • --ipc=host 누락 — TP에서 GPU 워커가 통신 못 해 startup이 멈춘다. 로그가 조용히 멈추면 이걸 먼저 의심하라.
  • tensor-parallel-size가 헤드 수와 안 나눠떨어짐 — 부팅 에러. 모델 config의 어텐션 헤드 수를 확인.
  • NVLink 없는데 TP 8 — PCIe 통신 병목으로 멀티 GPU인데 단일보다 느려질 수 있다. 토폴로지를 nvidia-smi topo -m으로 확인하라.
  • GPU 1장이면 TP 1 — 굳이 --tensor-parallel-size 1을 명시할 필요 없다(기본값).

베스트 프랙티스

GPU를 늘렸다고 처리량이 선형으로 늘진 않는다. TP는 통신 오버헤드 때문에 수확 체감이 있다. **"이 모델이 단일 GPU에 들어가면 단일로"**가 원칙 — 들어가는데도 TP를 쓰면 통신 비용만 더해진다. 분산은 "안 들어갈 때" 또는 "단일 GPU 처리량이 한계일 때"의 카드다.

8. 양자화 — AWQ·GPTQ·FP8로 큰 모델을 작은 GPU에

양자화는 가중치 정밀도를 낮춰(FP16 → INT4/INT8/FP8) 메모리와 대역폭을 줄이는 기법이다. 4섹션 표처럼 가중치 크기가 절반~1/4로 줄어, 더 큰 모델을 더 작은 GPU에, 또는 같은 GPU에 더 큰 KV 캐시와 함께 올릴 수 있다.

주요 포맷 비교

포맷비트특징적합
AWQ4-bitactivation-aware, 4비트에서 정확도 우수INT4 배포의 현재 베스트 프랙티스
GPTQ4-bit(주로)호환성 넓음, 모델 많음구형 GPU 포함 폭넓은 지원
FP88-bitH100 등 신형에서 속도 최고최신 데이터센터 GPU
INT88-bit정확도 손실 작음, 메모리 절감은 중간정확도 민감 워크로드

사용법 — 이미 양자화된 모델 서빙

가장 쉬운 길은 커뮤니티가 이미 양자화해 올린 모델을 그대로 서빙하는 것이다. vLLM이 포맷을 대개 자동 감지한다.

# AWQ로 양자화된 70B 모델 — 단일 80GB 카드에 가중치 ~35GB로 들어감
vllm serve <org>/Llama-3.1-70B-Instruct-AWQ \
  --quantization awq \
  --max-model-len 8192

FP8은 vLLM이 로딩 시점에 즉석 양자화도 지원한다(신형 GPU 한정).

vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --quantization fp8

메모리 효과 예시

  • 70B FP16: 가중치 ~140GB → 80GB GPU 2장(TP 2) 필요
  • 70B FP8: 가중치 ~70GB → 80GB 한 장에 빠듯하게
  • 70B AWQ(INT4): 가중치 ~35GB → 80GB 한 장 + 넉넉한 KV 캐시

즉 양자화는 GPU 수를 줄이거나(비용↓), 같은 GPU에서 동시성을 키운다(KV 풀↑).

정확도 트레이드오프

양자화는 공짜가 아니다. 비트를 줄일수록 출력 품질이 미세하게 떨어질 수 있다. 일반적 경향:

  • FP8/INT8 — 품질 저하가 보통 매우 작음
  • INT4(AWQ) — 잘 만든 AWQ는 대부분 태스크에서 실용적, 그러나 수치 정확도가 중요한 태스크(코드·수학·정밀 추출)에선 저하가 눈에 띌 수 있음

4비트 양자화가 특정 태스크에서 출력을 무너뜨리는 사례는 실재한다. 반드시 본인 평가셋으로 양자화 전/후 품질을 비교하고 받아들일 수 있는지 판단하라.

흔한 함정

  • --quantization 포맷 불일치 — 모델은 GPTQ인데 --quantization awq를 주면 에러. 모델 카드의 포맷을 정확히 맞춰라(대개 자동 감지되지만 명시가 안전).
  • 구형 GPU에서 FP8 — FP8은 신형 텐서 코어가 필요하다. 안 되는 카드에선 AWQ/GPTQ로.
  • "양자화했으니 무조건 빠르다"는 오해 — 메모리는 줄지만, 양자화/역양자화 오버헤드로 워크로드에 따라 속도 이득이 작을 수도 있다. 메모리가 병목이면 큰 이득, 연산이 병목이면 제한적.

베스트 프랙티스

H100급 신형이면 FP8을 기본으로(품질 손실 작고 빠름). 메모리가 빠듯하거나 구형 GPU면 AWQ INT4. 어느 쪽이든 배포 전 평가셋 비교는 생략하지 마라 — 특히 정밀 출력이 필요한 도메인에서.

9. 프로덕션화 — 벤치마크·관측·인증·게이트 모델

데모를 넘어 실제 서비스로 올리려면 측정·보안·운영을 갖춰야 한다.

부하 벤치마크 — 감이 아니라 숫자로

vLLM은 벤치마크 도구를 내장한다. 실제 트래픽 패턴으로 처리량·레이턴시를 측정해 튜닝 결정을 검증하라.

# 서버를 띄운 상태에서, 온라인 서빙 처리량 측정
vllm bench serve \
  --model qwen-7b \
  --base-url http://localhost:8000 \
  --dataset-name random \
  --num-prompts 500 \
  --request-rate 20   # 초당 20요청 부하

출력에서 P50/P99 TTFT, 토큰/초(throughput), 요청/초를 본다. max-num-seqs·max-num-batched-tokens를 바꿔 가며 이 숫자가 어떻게 움직이는지로 최적점을 찾는다. (오프라인 처리량은 vllm bench throughput.)

게이트 모델 (Llama 등) 인증

Llama처럼 접근 승인이 필요한 모델은 HuggingFace 토큰이 필요하다.

export HF_TOKEN=hf_xxx   # read 권한 토큰
vllm serve meta-llama/Llama-3.1-8B-Instruct
# Docker면: --env "HF_TOKEN=$HF_TOKEN"

API 인증

vLLM 내장 인증은 단일 키 수준이다. 외부 노출 시 반드시 켜라.

vllm serve <model> --api-key sk-내부공유키

⚠️ vLLM의 --api-key단순 공유 키 한 개다. 사용자별 키·레이트리밋·쿼터·감사 로그가 필요하면 앞단에 게이트웨이/리버스 프록시(예: Nginx + 인증 미들웨어, 또는 LiteLLM 같은 프록시)를 두라. vLLM을 0.0.0.0으로 직접 인터넷에 노출하지 말고, 방화벽·내부망 뒤에 둔다.

관측 (Observability)

vLLM은 Prometheus 메트릭 엔드포인트를 노출한다.

curl http://localhost:8000/metrics

주시할 핵심 지표:

  • running/waiting 요청 수 — waiting이 계속 쌓이면 동시성·KV 부족 신호
  • KV 캐시 사용률 — 100%에 붙으면 preemption 발생, 처리량 저하
  • TTFT / ITL — 사용자 체감 레이턴시
  • preemption 카운트 — 0이 아니면 KV가 부족해 요청이 쫓겨나는 중

Prometheus + Grafana로 대시보드를 붙이면 트래픽에 따른 병목이 한눈에 보인다.

안정 운영 체크리스트

항목권장
이미지 태그:latest 금지, 버전 고정
재시작 정책--restart unless-stopped(Docker)로 크래시 자동 복구
헬스체크/health 폴링, startup 타임아웃 넉넉히
모델 캐시HF 캐시 볼륨 마운트로 재다운로드 방지
리소스 격리GPU당 한 인스턴스, utilization 합 ≤ 1.0
롤아웃새 버전은 별도 포트로 띄워 벤치 후 트래픽 전환

흔한 함정

  • 벤치 없이 프로덕션 직행 — 단건 테스트는 통과해도 동시성 100에서 P99가 폭발한다. 반드시 목표 동시성으로 부하 테스트.
  • 메트릭 미수집 — preemption이 조용히 일어나며 처리량이 깎이는데 모르고 "느리다"고만 느낀다. /metrics를 처음부터 붙여라.
  • 버전 미고정 자동 업데이트 — vLLM은 마이너 릴리스마다 기본값/플래그가 바뀐다. 업그레이드는 의도적으로, 벤치 후에.

베스트 프랙티스

프로덕션 루프는 고정 이미지 → 벤치마크로 튜닝값 확정 → 메트릭으로 상시 관측 → 의도적 업그레이드다. 자체 호스팅의 가치(비용·주권·커스터마이징)는 이 운영 규율이 받쳐줄 때만 실현된다.


참고 자료