vLLM
vLLM은 대규모 언어 모델(LLM) 추론/서빙을 위한 고성능 엔진입니다.
특히 PagedAttention과 Continuous Batching을 통해, 동일한 GPU 자원에서 더 높은 처리량을 제공합니다.
빠른 추론 속도
OpenAI 호환 API(
/v1/chat/completions) 지원멀티 GPU 텐서 병렬화 지원
Qwen 계열 모델 서빙에 적합
왜 vLLM을 사용하는가?¶
LLM 서빙 성능은 크게 아래 요소에 의해 결정됩니다.
vLLM은 KV Cache를 효율적으로 관리해 요청 간 메모리 낭비를 줄이고, 배치 스케줄링을 최적화해 실제 서비스 처리량을 높입니다.
Ollama vs vLLM¶
주요 특성 비교¶
| 항목 | Ollama | vLLM |
|---|---|---|
| 설치 복잡도 | 매우 간단 (바이너리 설치) | 중간 (Python 환경 필요) |
| 사용 난이도 | 매우 쉬움 (ollama run) | 보통 (CLI/API 옵션 많음) |
| 동시 요청 처리 | 기본적 (병렬화 제한) | 우수 (최적화된 배칭) |
| 처리량 (대량 요청) | 낮음 | 높음 |
| 지연시간 (단일 요청) | 중간 | 낮음 |
| GPU 최적화 | 제한적 | 우수 (PagedAttention) |
| OpenAI 호환 | 제한적 | 완전함 |
| 메모리 효율성 | 기본적 | 우수 |
| 프로덕션 준비도 | 낮음 | 높음 |
활용 포인트¶
Ollama 권장 사용 사례¶
빠른 프로토타이핑: 모델을 빠르게 로드하고 테스트
로컬 개인 사용: 단건 요청이 주요 패턴
낮은 러닝 커브: 설정 없이 바로 실행 가능
소규모 배포: 1~2개 요청/초 이하
예: 개인 개발, 중소 기업의 간단한 챗봇
vLLM 권장 사용 사례¶
고성능 서빙: 많은 동시 사용자 처리
API 서버: OpenAI 호환 REST API 필요
배치 처리: 여러 요청을 효율적으로 처리
프로덕션 환경: 안정성과 성능이 중요
예: SaaS 서비스, 기업 내부 LLM 인프라, 고트래픽 서비스
전환 가이드¶
Ollama에서 vLLM으로 전환하는 신호:
동시 사용자 수가 10명 이상
평균 응답 시간 개선 필요
OpenAI API 호환성 필수
GPU 메모리 최적화 필요
환경 설정¶
Docker 기반 vLLM 환경을 설정합니다. CUDA 드라이버와 컨테이너 런타임만 갖춰져 있으면 됩니다.
최신 vLLM 이미지를 가져옵니다.
docker pull vllm/vllm-openai:latest모델 서버¶
Docker를 이용해 Qwen3 모델을 vLLM으로 서빙합니다. 단일 GPU 기준 설정입니다.
아래 예시는 Qwen/Qwen3-8B-Instruct 모델을 활용합니다.
# 단일 GPU 기준 Qwen3 모델을 vLLM 서버로 실행합니다.
docker run --gpus all \
-p 8000:8000 \
--ipc=host \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:latest \
Qwen/Qwen3-8B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--dtype bfloat16 \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--seed 42--gpus all: 모든 GPU를 컨테이너에 노출합니다.--ipc=host: 공유 메모리를 호스트와 공유해 텐서 병렬화 성능을 유지합니다.-v ~/.cache/huggingface:/root/.cache/huggingface: 모델 캐시를 호스트에 유지해 재다운로드를 방지합니다.
위 명령을 compose.yaml로 관리하면 옵션을 파일로 유지하고 반복 실행이 편해집니다.
# compose.yaml
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus: all
command: >
Qwen/Qwen3-8B-Instruct
--host 0.0.0.0
--port 8000
--dtype bfloat16
--max-model-len 8192
--gpu-memory-utilization 0.90
--seed 42# 서버 시작
docker compose up -d
# 로그 확인
docker compose logs -f vllm
# 서버 종료
docker compose down실행 옵션¶
| 옵션 | 설명 |
|---|---|
| 모델 ID(위치 인자) | 모델 이름 또는 경로를 지정합니다. |
--host | API 서버 바인딩 주소입니다. |
--port | API 서버 포트입니다. 기본값은 8000입니다. |
--dtype | 가중치/활성화 정밀도를 지정합니다(auto, float16, bfloat16 등). |
--max-model-len | 최대 컨텍스트 길이(입력+출력)를 지정합니다. |
--gpu-memory-utilization | GPU 메모리 사용 비율(기본 0.9)입니다. |
--seed | 재현성을 위한 시드입니다(기본 0). |
--max-num-seqs | 한 iteration에서 처리할 최대 시퀀스 수입니다. |
--max-num-batched-tokens | 한 iteration에서 처리할 최대 토큰 수입니다. |
--swap-space | GPU당 CPU swap 공간(GiB)입니다. |
--tensor-parallel-size (-tp) | 텐서 병렬 그룹 수(멀티 GPU 핵심)입니다. |
주요 응용 사례에 자주 쓰는 추가 옵션¶
응용 사례에 직접적인 옵션만 추렸습니다.
| 옵션 | 권장 시작값 | 주로 쓰는 상황 | 메모 |
|---|---|---|---|
--enable-chunked-prefill | 활성화 | 긴 프롬프트/RAG 입력이 많은 경우 | prefill을 나눠 지연시간 급증을 완화합니다. |
--async-scheduling | 활성화 | 동시 요청이 많은 온라인 서비스 | GPU 유휴 구간을 줄여 처리량 개선에 도움됩니다. |
--performance-mode | throughput 또는 balanced | 처리량 우선 vs 지연시간 우선 | interactivity는 저지연 중심입니다. |
--pipeline-parallel-size (-pp) | 1~2 | 단일 GPU 메모리로 큰 모델이 어려울 때 | TP와 함께 병렬 전략을 조합합니다. |
--data-parallel-size (-dp) | 2+ | 고QPS 수평 확장 | 복제된 엔진으로 요청 처리량을 높입니다. |
--api-server-count (-asc) | 1~4 | 프론트엔드 병목 완화 | 미지정 시 data_parallel_size를 따릅니다. |
--kv-cache-memory-bytes | 예: 10G | KV 캐시 메모리를 정밀 제어하고 싶을 때 | 설정 시 --gpu-memory-utilization보다 우선합니다. |
--cpu-offload-gb | 8~16 | GPU 메모리가 부족한 환경 | CPU-GPU 연결 대역폭이 중요합니다. |
--quantization (-q) | 모델/하드웨어별 상이 | 메모리 절감 및 더 큰 모델 서빙 | 정확도/속도 트레이드오프를 확인해야 합니다. |
--api-key | 운영 정책에 맞게 | 외부 노출 API 운영 | OpenAI 호환 API 보호에 필수적입니다. |
옵션 조합 예시¶
다양한 배포 시나리오에 대한 실전 명령어들입니다.
단일 GPU 동시 요청 최적화¶
동시 요청을 효율적으로 처리하기 위해 배치 크기와 메모리 활용 비율을 조정합니다.
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus: all
command: >
Qwen/Qwen3-8B-Instruct
--host 0.0.0.0
--port 8000
--dtype bfloat16
--max-model-len 8192
--gpu-memory-utilization 0.92
--max-num-seqs 32
--max-num-batched-tokens 4096
--enable-chunked-prefill
--async-scheduling
--performance-mode throughput
--seed 422-GPU 텐서 병렬¶
두 개 GPU를 활용해 더 큰 모델이나 긴 컨텍스트를 처리합니다.
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus:
- count: 2
command: >
Qwen/Qwen3-8B-Instruct
--tensor-parallel-size 2
--host 0.0.0.0
--port 8000
--dtype bfloat16
--max-model-len 8192
--gpu-memory-utilization 0.90
--max-num-seqs 64
--max-num-batched-tokens 8192
--performance-mode throughput
--seed 424-GPU 고동시성¶
텐서 병렬화와 데이터 병렬화를 결합하여 높은 처리량을 달성합니다.
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus: all
command: >
Qwen/Qwen3-8B-Instruct
--tensor-parallel-size 2
--data-parallel-size 2
--api-server-count 2
--host 0.0.0.0
--port 8000
--dtype bfloat16
--max-model-len 8192
--gpu-memory-utilization 0.90
--max-num-seqs 128
--max-num-batched-tokens 16384
--enable-chunked-prefill
--async-scheduling
--performance-mode throughput메모리 제약 환경¶
제한된 GPU 메모리에서 모델을 서빙하기 위해 CPU 오프로드와 캐시 최적화를 활용합니다.
services:
vllm:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus: all
command: >
Qwen/Qwen3-8B-Instruct
--host 0.0.0.0
--port 8000
--dtype float16
--max-model-len 4096
--kv-cache-memory-bytes 10G
--cpu-offload-gb 8
--swap-space 16API 호출 테스트¶
실행 중인 vLLM 서버에 프롬프트를 보내고 응답을 받습니다. curl과 Python의 두 가지 방법을 소개합니다.
curl로 호출¶
# OpenAI 호환 Chat Completions API 호출
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-8B-Instruct",
"messages": [
{"role": "system", "content": "당신은 딥러닝 튜터입니다."},
{"role": "user", "content": "vLLM의 장점을 3가지로 설명해줘."}
],
"temperature": 0.0,
"max_tokens": 256
}'Python으로 호출¶
OpenAI SDK를 이용해 vLLM과 상호작용합니다.
# OpenAI SDK를 vLLM 엔드포인트에 연결
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="EMPTY", # vLLM 로컬 서버는 임의 문자열 사용 가능
)
# 재현 가능성을 위해 temperature를 0으로 설정
response = client.chat.completions.create(
model="Qwen/Qwen3-8B-Instruct",
messages=[
{"role": "system", "content": "당신은 딥러닝 튜터입니다."},
{"role": "user", "content": "Qwen3를 서비스할 때 주의점을 알려줘."},
],
temperature=0.0,
max_tokens=256,
)
print(response.choices[0].message.content)성능 튜닝 포인트¶
GPU 메모리, 배치 크기, 정밀도 등을 최적화하여 처리량과 지연시간 사이의 균형을 맞웁니다.
| 항목 | 옵션 | 설명 |
|---|---|---|
| 최대 컨텍스트 길이 | --max-model-len | 길이를 줄이면 메모리 사용량 감소 |
| GPU 사용 비율 | --gpu-memory-utilization | OOM 방지를 위해 0.85~0.95 범위에서 조정 |
| 정밀도 | --dtype | bfloat16/float16로 메모리 절감 |
| 병렬화 | --tensor-parallel-size | 멀티 GPU 사용 시 처리량 향상 |
| 재현성 | --seed, temperature=0 | 결과 일관성 확보 |
멀티 GPU 예시:
# 2개 GPU를 사용해 Qwen3 대형 모델 서빙
docker run --gpus '"device=0,1"' \
-p 8000:8000 \
--ipc=host \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:latest \
Qwen/Qwen3-32B-Instruct \
--tensor-parallel-size 2 \
--host 0.0.0.0 \
--port 8000Ollama vs vLLM 동시 요청 처리 속도 비교¶
Ollama와 vLLM의 동시 요청 처리 성능을 실제로 측정하고 비교합니다. 동일한 Qwen3 8B 계열 모델로 테스트합니다.
비교 조건¶
| 항목 | 값 |
|---|---|
| 프롬프트 | 동일 문장 1개 |
| 생성 길이 | max_tokens=128 (num_predict=128) |
| 샘플링 | temperature=0.0 |
| 동시 요청 수 | 1, 4, 8, 16, 32 |
| 측정 지표 | req/s, p95 latency(ms) |
서버 실행¶
ollama pull qwen3:8b
OLLAMA_NUM_PARALLEL=8 ollama servedocker run --gpus all \
-p 8000:8000 \
--ipc=host \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:latest \
Qwen/Qwen3-8B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--dtype bfloat16 \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--seed 42벤치마크 스크립트¶
import argparse
import asyncio
import statistics
import time
import httpx
PROMPT = "vLLM과 Ollama의 동시 요청 처리 방식 차이를 3문장으로 설명해줘."
async def one_request(client: httpx.AsyncClient, target: str, max_tokens: int) -> float:
if target == "vllm":
url = "http://127.0.0.1:8000/v1/chat/completions"
payload = {
"model": "Qwen/Qwen3-8B-Instruct",
"messages": [{"role": "user", "content": PROMPT}],
"temperature": 0.0,
"max_tokens": max_tokens,
}
else:
url = "http://127.0.0.1:11434/api/generate"
payload = {
"model": "qwen3:8b",
"prompt": PROMPT,
"stream": False,
"options": {"temperature": 0.0, "num_predict": max_tokens},
}
t0 = time.perf_counter()
resp = await client.post(url, json=payload, timeout=180.0)
resp.raise_for_status()
return (time.perf_counter() - t0) * 1000.0
async def run_bench(target: str, concurrency: int, total_requests: int, max_tokens: int):
sem = asyncio.Semaphore(concurrency)
latencies = []
async with httpx.AsyncClient() as client:
async def worker():
async with sem:
return await one_request(client, target, max_tokens)
start = time.perf_counter()
results = await asyncio.gather(
*[worker() for _ in range(total_requests)],
return_exceptions=True,
)
elapsed = time.perf_counter() - start
for r in results:
if not isinstance(r, Exception):
latencies.append(r)
success = len(latencies)
rps = success / elapsed if elapsed > 0 else 0.0
p50 = statistics.median(latencies) if latencies else 0.0
p95 = (
statistics.quantiles(latencies, n=100)[94]
if len(latencies) >= 20
else (max(latencies) if latencies else 0.0)
)
print(
f"[{target}] c={concurrency:>2} | success={success:>3}/{total_requests:<3} "
f"| req/s={rps:>6.2f} | p50={p50:>7.1f}ms | p95={p95:>7.1f}ms"
)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("--target", choices=["ollama", "vllm"], required=True)
parser.add_argument("--concurrency", type=int, nargs="+", default=[1, 4, 8, 16, 32])
parser.add_argument("--total-requests", type=int, default=64)
parser.add_argument("--max-tokens", type=int, default=128)
args = parser.parse_args()
for c in args.concurrency:
await run_bench(args.target, c, args.total_requests, args.max_tokens)
if __name__ == "__main__":
asyncio.run(main())실행:
pip install -U httpx
python benchmark_concurrency.py --target ollama --concurrency 1 4 8 16 32 --total-requests 64
python benchmark_concurrency.py --target vllm --concurrency 1 4 8 16 32 --total-requests 64결과 기록 템플릿¶
| 서버 | 동시 요청 수 | req/s | p95(ms) |
|---|---|---|---|
| Ollama | 1 | ||
| Ollama | 4 | ||
| Ollama | 8 | ||
| Ollama | 16 | ||
| Ollama | 32 | ||
| vLLM | 1 | ||
| vLLM | 4 | ||
| vLLM | 8 | ||
| vLLM | 16 | ||
| vLLM | 32 |
일반적으로 동시 요청 수가 커질수록 vLLM의 처리량 증가폭이 더 크게 나타납니다.
자주 발생하는 문제¶
배포 과정에서 마주할 수 있는 주요 이슈와 해결 방법을 정리합니다.
CUDA/드라이버 오류
NVIDIA 드라이버, CUDA, PyTorch/vLLM 버전 호환성을 먼저 확인합니다.
OOM(Out of Memory)
--max-model-len축소작은 모델 사용
동시 요청 수 제한
응답 지연 증가
긴 입력 프롬프트 축소
배치 전략 및 GPU 수 조정
다음 단계¶
vLLM 배포 이후 실전에서 활용할 수 있는 방향들을 소개합니다.
배포 전 체크리스트¶
vLLM 서버 운영을 위한 사전 확인 사항입니다.
GPU 메모리 충분성 검증: 모델 용량과 최대 컨텍스트 길이(
--max-model-len)를 고려동시 요청 목표 설정: 예상 QPS에 따라
--max-num-seqs와--max-num-batched-tokens결정프롬프트 길이 정의: 평균 입력 길이와 생성 길이(
max_tokens) 기록CPU 및 호스트 메모리 여유: CPU 오프로드 옵션 필요 시 확보
API 보안 설정: 외부 노출 시
--api-key지정은 필수
다중 모델 서빙¶
여러 버전의 Qwen3 모델을 동시에 서빙할 수 있습니다.
services:
vllm-8b:
image: vllm/vllm-openai:latest
ports:
- "8000:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus:
- count: 1
command: >
Qwen/Qwen3-8B-Instruct
--host 0.0.0.0
--port 8000
vllm-32b:
image: vllm/vllm-openai:latest
ports:
- "8001:8000"
ipc: host
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
gpus:
- count: 2
command: >
Qwen/Qwen3-32B-Instruct
--tensor-parallel-size 2
--host 0.0.0.0
--port 8000프론트엔드는 요청 특성에 따라 8B(빠름)와 32B(정확함) 중 선택합니다.
RAG 통합¶
RAG 파이프라인과 연결하여 검색 증강 생성(RAG) 파이프라인을 구성합니다.
파이프라인 구조:
문서 저장소 → 벡터 임베딩 → 벡터 DB → 검색기 → 프롬프트 구성 → vLLM API → 최종 응답핵심 단계:
임베딩: 문서를 벡터로 변환 (예:
sentence-transformers)검색: 사용자 쿼리 관련 문서 상위 K개 반환
컨텍스트: 검색된 문서를 프롬프트에 포함
생성: vLLM 서버로 최종 응답 생성
프로덕션 배포 팁¶
안정적인 운영을 위한 실전 노하우입니다.
로그 관리:
docker compose logs --follow또는 ELK 스택 연동헬스 체크: 별도 스크립트에서
/health엔드포인트 주기적 호출자동 재시작:
compose.yaml에restart_policy: always설정모니터링: Prometheus/Grafana로 GPU 메모리, 처리량, p95 지연시간 추적
버전 관리: 모델 변경 시 Docker 이미지
tag로 기록